Java-141 深入浅出 MySQL Spring事务失效的常见场景与解决方案详解(3)
点一下关注吧!!!非常感谢!!持续更新!!!
🚀 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案例 详解
事务失效场景(3)
参考来源
● https://heapdump.cn/article/5542790
续接上节
rollbackFor 配置错误
在 Spring 事务管理中,@Transactional
注解的 rollbackFor
属性用于指定哪些异常类型会触发事务回滚。以下是更详细的说明:
-
默认回滚规则:
- 默认情况下,Spring 事务会在遇到
RuntimeException
或其子类以及Error
及其子类时自动回滚。 - 例如:
NullPointerException
(RuntimeException
子类)或OutOfMemoryError
(Error
子类)都会触发自动回滚。
- 默认情况下,Spring 事务会在遇到
-
rollbackFor 的明确指定:
- 当显式指定
rollbackFor=Error.class
时,只有Error
及其子类异常会触发回滚。 - 其他异常类型(包括
Exception
)不会触发回滚,即使它们是未检查异常。
- 当显式指定
-
异常继承关系的影响:
Error
和Exception
都是Throwable
的子类,但彼此没有继承关系。- 如果抛出的异常是
Exception
或其子类(如IOException
),但rollbackFor
仅指定了Error.class
,则事务不会回滚。
-
典型问题示例:
@Transactional(rollbackFor = Error.class)public void updateData() throws Exception {// 业务逻辑throw new Exception("自定义异常"); // 不会触发回滚}
- 这里抛出的是
Exception
,而rollbackFor
仅覆盖Error
,因此事务不会回滚。
- 解决方案:
- 如果需要回滚
Exception
,应明确指定:
- 如果需要回滚
@Transactional(rollbackFor = {Error.class, Exception.class})```- 或者使用更通用的方式:```java@Transactional(rollbackFor = Throwable.class) // 覆盖所有异常
- 实际应用场景:
- 在需要严格区分异常处理的场景中(如仅对系统级错误回滚),可以限定
rollbackFor=Error.class
。 - 在大多数业务场景中,建议至少包含
RuntimeException
和自定义业务异常。
- 在需要严格区分异常处理的场景中(如仅对系统级错误回滚),可以限定
这里我们自定了几个异常类,BaseException 继承了 Exception,同时有两个子类:
● ParamException 参数异常
● ResultException 结果异常
此时,rollbackFor 只有 ResultException,这种情况下,会导致事务失效不会回滚:
@Service
@RequiredArgsConstructor
public class TestServiceImpl implements TestService {private final UnitInfoMapper unitInfoMapper;@Transactional(rollbackFor = ResultException.class)@Overridepublic String test01() throws Exception {UnitInfo unitInfo = UnitInfo.builder().unitName("wzk").unitCode("icu").build();unitInfoMapper.insertUnitInfo(unitInfo);if (1 == 1) {throw new ParamException("ERROR");}return "ok";}
}
执行完后,数据库的情况:
事务注解被覆盖导致事务失效
事务失效问题分析:子类覆盖父类事务的情况
问题描述
在Spring框架的事务管理中,当子类重写父类带有事务注解(@Transactional)的方法时,可能会出现事务失效的情况。这是因为Spring的事务管理是基于AOP代理实现的,而子类方法的覆盖行为可能导致事务注解未被正确识别和应用。
典型场景示例
@Service
public class ParentService {@Transactionalpublic void parentMethod() {// 业务逻辑}
}@Service
public class ChildService extends ParentService {@Overridepublic void parentMethod() {// 重写父类方法,但未添加@Transactional注解super.parentMethod();}
}
失效原因分析
- 代理机制问题:Spring默认使用JDK动态代理或CGLIB代理来实现事务管理
- 注解继承:@Transactional注解默认不会被子类继承
- 调用方式:如果子类通过this调用方法,会绕过代理对象
解决方案
- 显式添加事务注解:
@Override
@Transactional // 显式添加事务注解
public void parentMethod() {super.parentMethod();
}
- 使用接口定义事务方法:
public interface TransactionalService {@Transactionalvoid transactionalMethod();
}@Service
public class ActualService implements TransactionalService {@Overridepublic void transactionalMethod() {// 实现逻辑}
}
- 配置代理模式:
@EnableTransactionManagement(proxyTargetClass = true) // 强制使用CGLIB代理
- 避免内部调用:
@Service
public class SomeService {public void outerMethod() {innerMethod(); // 错误:内部调用会绕过代理this.innerMethod(); // 同样错误}@Transactionalpublic void innerMethod() {// 业务逻辑}
}
最佳实践
- 将事务方法定义在接口中
- 避免在类内部调用事务方法
- 使用final修饰不打算被重写的事务方法
- 考虑使用AspectJ模式替代代理模式
- 在重写方法时显式声明事务属性
调试技巧
- 开启DEBUG日志查看事务创建和提交情况
- 检查实际调用的方法是否经过代理
- 使用TransactionSynchronizationManager.isActualTransactionActive()验证事务状态
比如,在 Service 上,我们定义了正常的类:
public interface TestService {@TransactionalString test01() throws Exception;}
而在实现类上,我们重新定义了事务的情况:
@Service
@RequiredArgsConstructor
public class TestServiceImpl implements TestService {private final UnitInfoMapper unitInfoMapper;@Transactional(rollbackFor = ResultException.class)@Overridepublic String test01() throws Exception {UnitInfo unitInfo = UnitInfo.builder().unitName("wzk").unitCode("icu").build();unitInfoMapper.insertUnitInfo(unitInfo);if (1 == 1) {throw new ParamException("ERROR");}return "ok";}
}
此时执行后,没有回滚,数据库写入了数据:
嵌套事务
事务失效问题:嵌套事务场景分析
嵌套事务的基本原理
嵌套事务是指在一个事务方法(A)中调用另一个事务方法(B)时发生的事务交互。在Spring等框架中,默认的事务传播行为是REQUIRED
,即:
- 如果当前存在事务,就加入该事务
- 如果当前没有事务,就新建一个事务
典型失效场景
当方法A调用方法B时:
- 方法A开启事务(事务A)
- 方法B被调用,由于传播行为是
REQUIRED
,方法B会加入事务A - 方法B执行过程中发生异常并回滚
- 由于方法B是加入方法A的事务,所以整个事务A都会被回滚
具体示例
@Service
public class OrderService {@Transactionalpublic void processOrder(Order order) { // 方法A// 订单处理逻辑inventoryService.reduceStock(order); // 调用方法B// 其他订单处理逻辑}
}@Service
public class InventoryService {@Transactionalpublic void reduceStock(Order order) { // 方法B// 库存扣减逻辑if (stock < order.getQuantity()) {throw new RuntimeException("库存不足"); // 触发回滚}// ...}
}
解决方案
- 修改传播行为:将方法B的事务传播行为改为
REQUIRES_NEW
,这样方法B会开启新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)public void reduceStock(Order order) { ... }
- 捕获内部异常:在方法A中捕获方法B的异常,避免异常传播到方法A
try {inventoryService.reduceStock(order);} catch (Exception e) {// 处理异常但不抛出}
- 使用编程式事务:在需要更细粒度控制时使用编程式事务管理
实际影响
这种嵌套事务问题会导致:
- 主业务逻辑被意外回滚
- 数据不一致性
- 难以追踪的问题根源
- 业务逻辑与预期不符
比如如下情况,会导致两条数据都没有插入,回滚掉了:
@Service
@RequiredArgsConstructor
public class TestServiceImpl implements TestService {private final UnitInfoMapper unitInfoMapper;@Transactional@Overridepublic String test01() throws Exception {UnitInfo unitInfo = UnitInfo.builder().unitName("wzk").unitCode("icu").build();unitInfoMapper.insertUnitInfo(unitInfo);test02();return "ok";}@Overridepublic String test02() {UnitInfo unitInfo = UnitInfo.builder().unitName("wzk").unitCode("icu").build();unitInfoMapper.insertUnitInfo(unitInfo);if (1 == 1) {throw new RuntimeException("ERROR");}return "ok";}
}
下面这种写法结果也是一样的,都回滚掉了:
@Service
@RequiredArgsConstructor
public class TestServiceImpl implements TestService {private final UnitInfoMapper unitInfoMapper;@Transactional@Overridepublic String test01() throws Exception {UnitInfo unitInfo = UnitInfo.builder().unitName("wzk").unitCode("icu").build();unitInfoMapper.insertUnitInfo(unitInfo);test02();if (1 == 1) {throw new RuntimeException("ERROR");}return "ok";}@Overridepublic String test02() {UnitInfo unitInfo = UnitInfo.builder().unitName("wzk").unitCode("icu").build();unitInfoMapper.insertUnitInfo(unitInfo);return "ok";}
}