@Transactional 失效场景:面试最爱挖的 6 个坑
面试官:“你遇到过
@Transactional失效的情况吗?”
你:“遇到过。比如方法不是 public、同类中方法互相调用、异常被 try-catch 吞掉、传播属性设置错误、数据库引擎不支持事务、抛出了非 RuntimeException 异常等。”
面试官:“那你能具体说说为什么同类调用会失效吗?底层原理是什么?”
你:“……”
很多人能列举失效场景,但一问到“为什么”就卡壳了。本文从 Spring AOP 代理原理出发,把 6 个常见失效场景彻底讲透,并给出解决方案。
一、@Transactional失效的本质原因
Spring 事务管理基于AOP 动态代理。当一个 Bean 被代理后,只有通过代理对象调用方法,才会触发事务增强逻辑。直接通过this调用目标对象的方法,不会经过代理,因此@Transactional完全无效。
理解了这个根本原因,下面 6 个失效场景就都能解释通了。
二、6 大失效场景详解
场景 1:方法非 public
现象:@Transactional标注在private、protected或default访问权限的方法上,事务不生效。
原理:Spring 默认使用 CGLIB 或 JDK 动态代理。对于 CGLIB,它通过生成子类覆盖目标方法实现增强,但private方法无法被子类覆盖;对于 JDK 动态代理,代理类只实现接口方法,private方法不在接口中。Spring 会忽略非public方法的@Transactional注解,且不报错。
@ServicepublicclassOrderService{@Transactional// 失效!方法不是 publicprivatevoidupdateOrder(){}}解决方案:确保事务方法声明为public。如果需要保护内部方法,可以将其提取到单独的 Service 类中。
场景 2:同类中方法互相调用(自调用)
现象:同一个类中,方法 A(无@Transactional)调用方法 B(有@Transactional),事务不生效。
@ServicepublicclassOrderService{publicvoidmethodA(){methodB();// 直接调用,不会经过代理,事务失效}@TransactionalpublicvoidmethodB(){}}原理:methodA通过this.methodB()调用,this是原始对象,不是代理对象。代理对象只有在外部调用时才会生效。
解决方案:
- 注入自身代理(推荐):
@ServicepublicclassOrderService{@AutowiredprivateOrderServiceselfProxy;publicvoidmethodA(){selfProxy.methodB();// 通过代理调用}}- 从 Spring 容器中获取代理:
OrderServiceproxy=(OrderService)AopContext.currentProxy();proxy.methodB();- 将方法 B 提取到另一个 Service(最清晰,符合单一职责)。
场景 3:异常被 try-catch 吃掉,没有抛出
现象:方法内部 catch 了异常但没有重新抛出,事务不会回滚。
@TransactionalpublicvoidupdateOrder(){try{// 执行数据库操作,可能抛出异常}catch(Exceptione){log.error("异常被捕获,未抛出",e);// 没有重新抛出,事务会正常提交!}}原理:Spring 事务管理器通过检测方法抛出的异常来决定是否回滚。如果异常被捕获且未重新抛出,Spring 认为方法执行成功,会提交事务。
解决方案:
- 要么在 catch 块中抛出异常(
throw e;) - 要么手动标记事务为回滚:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
场景 4:传播属性设置错误
现象:使用了Propagation.NOT_SUPPORTED、Propagation.NEVER等传播行为,导致事务不存在。
@Transactional(propagation=Propagation.NOT_SUPPORTED)publicvoidmethodB(){}// 这个方法永远在非事务环境下执行如果调用方有事务,会被挂起;如果调用方无事务,也以非事务执行。总之,方法 B 内不会有事务。
解决方案:确认传播行为符合预期。大部分业务场景应使用默认的REQUIRED。
场景 5:数据库引擎不支持事务
现象:代码和配置都没问题,但事务就是不生效。常见于 MySQL 使用 MyISAM 引擎。
原理:MyISAM 引擎不支持事务(没有 undo log),而 InnoDB 支持。如果表的存储引擎是 MyISAM,无论怎么配置@Transactional,都不会有回滚效果。
解决方案:
- 检查表引擎:
SHOW TABLE STATUS WHERE Name = 'your_table'; - 修改为 InnoDB:
ALTER TABLE your_table ENGINE=InnoDB; - 在创建表时指定引擎:
ENGINE=InnoDB
场景 6:抛出了非 RuntimeException 异常
现象:方法抛出了Exception(非RuntimeException子类),事务没有回滚。
@Transactionalpublicvoidmethod()throwsException{// ...thrownewException("checked exception");}原理:Spring 事务回滚的默认行为是:只对RuntimeException和Error进行回滚,对checked exception(Exception的子类但非RuntimeException)默认不进行回滚。
解决方案:
- 配置
rollbackFor属性:
@Transactional(rollbackFor=Exception.class)- 或者抛出
RuntimeException的子类。
三、其他容易被忽略的失效场景
7. 多数据源配置错误
如果配置了多个TransactionManager,但没有指定@Transactional("txManagerName"),Spring 可能使用了错误的事务管理器,导致事务不生效。
8. 事务方法所在的 Bean 没有被 Spring 管理
类上没有@Service、@Component等注解,或者通过new手动创建对象,@Transactional完全无效。
9. 切面顺序导致事务增强被覆盖
如果自定义了 AOP 切面,且执行顺序在事务切面之前,且切面内部 catch 了异常,可能导致事务切面无法接收到异常。
10. 方法内部使用了异步线程
在事务方法内新开一个线程执行数据库操作,新线程的数据库连接与当前事务不绑定,操作不会参与当前事务。
四、如何快速排查事务失效?
- 检查方法是否为 public
- 检查是否通过代理调用(排除自调用)
- 检查异常是否被吞掉(看日志)
- 检查传播行为(默认 REQUIRED)
- 检查数据库引擎(InnoDB)
- 检查异常类型(是否在 rollbackFor 范围内)
- 开启 Spring 事务日志:
logging.level.org.springframework.transaction=DEBUG logging.level.org.springframework.orm.jpa=DEBUG观察日志中事务的创建、提交、回滚信息。
五、总结表
| 失效场景 | 根本原因 | 解决方案 |
|---|---|---|
| 方法非 public | 代理无法拦截非 public 方法 | 改为 public |
| 同类自调用 | 未通过代理对象调用 | 注入自身代理或拆分到新类 |
| 异常被 catch 吃掉 | Spring 未感知到异常 | 抛出异常或手动回滚 |
| 传播属性错误 | 事务不存在(如 NOT_SUPPORTED) | 确认传播行为,默认 REQUIRED |
| 数据库引擎不支持 | MyISAM 等不支持事务 | 改为 InnoDB |
| 非 RuntimeException | 默认不回滚 checked exception | 配置 rollbackFor = Exception.class |
一句话记住失效场景:非 public、自调用、吞异常、传播错、引擎差、异常选错。
希望这篇文章能帮你避开@Transactional的常见陷阱,在面试和开发中都能从容应对。如果需要进一步了解分布式事务或编程式事务,欢迎继续讨论。