Java-140 深入浅出 MySQL Spring事务失效的常见场景与解决方案详解(2)
点一下关注吧!!!非常感谢!!持续更新!!!
🚀 AI篇持续更新中!(长期更新)
AI炼丹日志-31- 千呼万唤始出来 GPT-5 发布!“快的模型 + 深度思考模型 + 实时路由”,持续打造实用AI工具指南!📐🤖
💻 Java篇正式开启!(300篇)
目前2025年09月29日更新到:
Java-136 深入浅出 MySQL Spring Boot @Transactional 使用指南:事务传播、隔离级别与异常回滚策略
MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务正在更新!深入浅出助你打牢基础!
📊 大数据板块已完成多项干货更新(300篇):
包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!
大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解
事务失效场景(2)
参考来源
● https://heapdump.cn/article/5542790
续接上节
访问权限不是 public
在 Spring 框架中,事务失效是一个常见的问题,特别是在使用基于注解的声明式事务管理时。这种情况通常是由于 Spring 的事务机制底层实现原理导致的。具体来说:
-
实现机制分析:
- Spring 的事务管理是通过 AOP(面向切面编程)机制实现的
- AOP 的核心实现方式是使用动态代理(Dynamic Proxy)技术
- 对于 JDK 动态代理,要求目标方法必须是 public 的才能被代理
-
具体失效原因:
- 当方法不是 public 时,在 AbstractFallbackTransactionAttributeSource 类的 computeTransactionAttribute() 方法中
- 会直接返回 null,表示不应用事务属性
- 从而导致事务功能失效
-
源码分析路径:
AbstractFallbackTransactionAttributeSource └── computeTransactionAttribute()└── 检查方法修饰符是否为 public├── 是:继续处理事务属性└── 否:返回 null
-
实际应用中的典型场景:
- 开发者在 private 或 protected 方法上添加 @Transactional 注解
- 在同一个类内部调用带有 @Transactional 注解的方法(这种自调用也会导致事务失效)
- 在非 public 方法上使用 Spring 的事务注解
-
解决方案:
- 确保所有需要事务管理的方法都是 public 的
- 避免在同一个类中进行自调用
- 考虑使用 AspectJ 模式代替动态代理模式(需要额外配置)
- 对于内部调用,可以通过 AopContext.currentProxy() 获取代理对象
建议开发者在遇到事务失效问题时,首先检查方法的访问修饰符是否符合要求,这是最常见的事务失效原因之一。在 Spring 框架中,事务失效是一个常见的问题,特别是在使用基于注解的声明式事务管理时。这种情况通常是由于 Spring 的事务机制底层实现原理导致的。具体来说:
-
实现机制分析:
- Spring 的事务管理是通过 AOP(面向切面编程)机制实现的
- AOP 的核心实现方式是使用动态代理(Dynamic Proxy)技术
- 对于 JDK 动态代理,要求目标方法必须是 public 的才能被代理
-
具体失效原因:
- 当方法不是 public 时,在 AbstractFallbackTransactionAttributeSource 类的 computeTransactionAttribute() 方法中
- 会直接返回 null,表示不应用事务属性
- 从而导致事务功能失效
-
源码分析路径:
AbstractFallbackTransactionAttributeSource └── computeTransactionAttribute()└── 检查方法修饰符是否为 public├── 是:继续处理事务属性└── 否:返回 null
-
实际应用中的典型场景:
- 开发者在 private 或 protected 方法上添加 @Transactional 注解
- 在同一个类内部调用带有 @Transactional 注解的方法(这种自调用也会导致事务失效)
- 在非 public 方法上使用 Spring 的事务注解
-
解决方案:
- 确保所有需要事务管理的方法都是 public 的
- 避免在同一个类中进行自调用
- 考虑使用 AspectJ 模式代替动态代理模式(需要额外配置)
- 对于内部调用,可以通过 AopContext.currentProxy() 获取代理对象
建议开发者在遇到事务失效问题时,首先检查方法的访问修饰符是否符合要求,这是最常见的事务失效原因之一。
数据库存储引擎不支持事务
Spring 事务失效的原因分析:存储引擎选择
事务失效的根本原因
Spring 事务的底层实现实际上是依赖于数据库本身的事务支持机制。当我们在代码中使用@Transactional
注解时,Spring 只是通过AOP代理对数据库操作进行了事务管理的封装,其本质仍然是调用数据库提供的事务功能。
MySQL存储引擎的事务支持差异
在MySQL数据库中,不同的存储引擎对事务的支持程度存在显著差异:
-
MyISAM存储引擎
- 不支持事务处理
- 不支持行级锁(只有表锁)
- 不支持外键约束
- 性能较高,适合读多写少的场景
- 示例:
CREATE TABLE my_table (...) ENGINE=MyISAM;
-
InnoDB存储引擎
- 完全支持ACID事务特性
- 支持行级锁
- 支持外键约束
- 提供崩溃恢复能力
- 示例:
CREATE TABLE my_table (...) ENGINE=InnoDB;
开发实践建议
-
表设计阶段检查:
- 在创建表时显式指定存储引擎类型
- 示例SQL:
SHOW TABLE STATUS LIKE 'table_name'\G
可查看现有表的存储引擎
-
数据库全局配置:
- 可以在my.cnf/my.ini配置文件中设置默认存储引擎
[mysqld]default-storage-engine=InnoDB
- 转换现有表:
- 对于已存在的MyISAM表,可以使用ALTER TABLE命令转换:
ALTER TABLE table_name ENGINE=InnoDB;
- Spring配置验证:
- 在应用启动时添加检查逻辑,确保关键业务表使用InnoDB引擎
- 可以通过JDBC元数据API编程检查表属性
其他可能导致事务失效的因素
除了存储引擎选择外,还应注意:
- 事务传播行为设置不当
- 异常捕获处理方式不正确
- 方法访问权限问题(如非public方法)
- 同一个类内方法调用导致的代理失效# Spring 事务失效的原因分析:存储引擎选择
事务失效的根本原因
Spring 事务的底层实现实际上是依赖于数据库本身的事务支持机制。当我们在代码中使用@Transactional
注解时,Spring 只是通过AOP代理对数据库操作进行了事务管理的封装,其本质仍然是调用数据库提供的事务功能。
MySQL存储引擎的事务支持差异
在MySQL数据库中,不同的存储引擎对事务的支持程度存在显著差异:
-
MyISAM存储引擎
- 不支持事务处理
- 不支持行级锁(只有表锁)
- 不支持外键约束
- 性能较高,适合读多写少的场景
- 示例:
CREATE TABLE my_table (...) ENGINE=MyISAM;
-
InnoDB存储引擎
- 完全支持ACID事务特性
- 支持行级锁
- 支持外键约束
- 提供崩溃恢复能力
- 示例:
CREATE TABLE my_table (...) ENGINE=InnoDB;
开发实践建议
-
表设计阶段检查:
- 在创建表时显式指定存储引擎类型
- 示例SQL:
SHOW TABLE STATUS LIKE 'table_name'\G
可查看现有表的存储引擎
-
数据库全局配置:
- 可以在my.cnf/my.ini配置文件中设置默认存储引擎
[mysqld]default-storage-engine=InnoDB
- 转换现有表:
- 对于已存在的MyISAM表,可以使用ALTER TABLE命令转换:
ALTER TABLE table_name ENGINE=InnoDB;
- Spring配置验证:
- 在应用启动时添加检查逻辑,确保关键业务表使用InnoDB引擎
- 可以通过JDBC元数据API编程检查表属性
其他可能导致事务失效的因素
除了存储引擎选择外,还应注意:
- 事务传播行为设置不当
- 异常捕获处理方式不正确
- 方法访问权限问题(如非public方法)
- 同一个类内方法调用导致的代理失效
注解配置错误
事务失效的常见场景之一是当方法参数中设置了readOnly = True
属性。这种情况下,Spring会将事务标记为只读事务(read-only transaction),其主要表现和影响包括:
-
只读事务特性:
- 数据库会针对只读事务进行优化
- 通常会将事务隔离级别设置为最低(如READ_UNCOMMITTED)
- Hibernate等ORM框架会禁用脏检查机制
-
增删改操作限制:
- 当尝试执行INSERT、UPDATE或DELETE等写操作时
- 底层会抛出异常,如"Read-only transaction cannot perform write operations"
- 具体错误信息可能因数据库驱动或ORM框架而异
-
典型应用场景:
@Transactional(readOnly = true)public User getUserById(Long id) {// 只允许查询操作return userRepository.findById(id);}
-
常见错误配置:
- 误将写操作方法标记为只读
- 在Service层方法上全局设置readOnly=true
- 没有根据实际业务需求区分读写事务
-
解决方案:
- 严格区分读写方法的事务配置
- 写操作方法应该去掉readOnly属性或显式设置为false
- 可以使用@Transactional的默认配置(不指定readOnly)来允许写操作
注意:不同持久化框架对只读事务的实现可能略有差异,但基本原理相同,即只读事务不能执行任何修改数据库状态的操作。事务失效的常见场景之一是当方法参数中设置了readOnly = True
属性。这种情况下,Spring会将事务标记为只读事务(read-only transaction),其主要表现和影响包括:
-
只读事务特性:
- 数据库会针对只读事务进行优化
- 通常会将事务隔离级别设置为最低(如READ_UNCOMMITTED)
- Hibernate等ORM框架会禁用脏检查机制
-
增删改操作限制:
- 当尝试执行INSERT、UPDATE或DELETE等写操作时
- 底层会抛出异常,如"Read-only transaction cannot perform write operations"
- 具体错误信息可能因数据库驱动或ORM框架而异
-
典型应用场景:
@Transactional(readOnly = true)public User getUserById(Long id) {// 只允许查询操作return userRepository.findById(id);}
-
常见错误配置:
- 误将写操作方法标记为只读
- 在Service层方法上全局设置readOnly=true
- 没有根据实际业务需求区分读写事务
-
解决方案:
- 严格区分读写方法的事务配置
- 写操作方法应该去掉readOnly属性或显式设置为false
- 可以使用@Transactional的默认配置(不指定readOnly)来允许写操作
注意:不同持久化框架对只读事务的实现可能略有差异,但基本原理相同,即只读事务不能执行任何修改数据库状态的操作。
@Service
@RequiredArgsConstructor
public class TestServiceImpl implements TestService {private final UnitInfoMapper unitInfoMapper;@Transactional(readOnly = true)@Overridepublic String test01() {UnitInfo unitInfo = UnitInfo.builder().unitName("wzk").unitCode("icu").build();unitInfoMapper.insertUnitInfo(unitInfo);return "ok";}
}
直接运行报错了:
java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowedat com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129) ~[mysql-connector-java-8.0.19.jar:8.0.19]at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.19.jar:8.0.19]at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89) ~[mysql-connector-java-8.0.19.jar:8.0.19]at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63) ~[mysql-connector-java-8.0.19.jar:8.0.19]
事务超时时间过短
timeout 参数设置的过短,比如 0.1 秒,导致事务直接超时。
错误的传播机制
比如使用了 NOT_SUPPORTED 的传播机制,虽然代码中报了异常,但是数据库的数据还是插入进去了。
@Service
@RequiredArgsConstructor
public class TestServiceImpl implements TestService {private final UnitInfoMapper unitInfoMapper;@Transactional(propagation = Propagation.NOT_SUPPORTED)@Overridepublic String test01() {UnitInfo unitInfo = UnitInfo.builder().unitName("wzk").unitCode("icu").build();unitInfoMapper.insertUnitInfo(unitInfo);if (1 == 1) {throw new RuntimeException("ERROR");}return "ok";}
}
数据库的结果: