@Transactional 注解在哪些情况下会导致事务失效(即不回滚)?
在 Spring 框架中,@Transactional 注解失效(即发生异常时不回滚)是开发中非常常见的问题,也是面试的高频考点。
@Transactional 的底层实现依赖于 Spring AOP(动态代理) 和 数据库本身的事务支持。只要破坏了这两者中的任何一个环节,事务就会失效。
以下是导致 @Transactional 事务失效的常见场景,按原因分类总结:
一、 方法签名或修饰符问题
1. 方法不是 public 的
- 原因:Spring AOP 默认只会对
public方法进行拦截并生成代理。如果将@Transactional加在private、protected或包可见(default)的方法上,Spring 不会报错,但事务根本不会生效。 - 解决:确保加了注解的方法是
public的。
2. 方法被 final 或 static 修饰
- 原因:Spring AOP 通常基于 CGLIB 实现(针对没有实现接口的类)。CGLIB 是通过生成子类来创建代理对象的。
final方法不能被重写,static方法属于类级别的调用,无法被子类拦截,因此代理机制会失效。 - 解决:去除
final或static修饰符。
二、 AOP 代理问题(内部调用)
3. 同一个类中的方法内部调用(最常见的坑)
- 场景:类
A中有方法a()和方法b()。b()上加了@Transactional,但在a()中直接调用了b()。外部调用a()时,b()的事务失效。 - 原因:Spring 事务是基于代理对象工作的。外部调用时,调用的是代理对象的方法,代理对象会管理事务。但在类内部
a()调用b()时,实际上相当于this.b(),调用的是目标对象自身的方法,绕过了 Spring 的代理类,因此事务切面不会执行。 - 解决:
- 方案A:将
b()移到另一个被 Spring 管理的 Service 类中进行调用。 - 方案B:在类内部通过注入自身来调用(Spring 4.3+ 支持)。
- 方案C:通过
AopContext.currentProxy()获取当前代理对象来调用(需要配置@EnableAspectJAutoProxy(exposeProxy = true))。
- 方案A:将
三、 异常处理问题
4. 异常被 catch 捕获且未抛出
- 场景:在事务方法中使用
try-catch捕获了异常,但没有继续往外抛出(throw)。 - 原因:Spring AOP 只有捕获到方法抛出的异常时,才会触发回滚机制。如果异常在方法内部被吃掉了,Spring 认为方法正常执行完毕,就会执行提交(Commit)。
- 解决:如果在
catch块中处理了逻辑,必须显式地抛出异常(如throw new RuntimeException(e);),或者手动回滚事务:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();。
5. 抛出了非 RuntimeException(检查型异常)
- 原因:Spring 默认只在遇到
RuntimeException(运行时异常)和Error时才会回滚。如果你抛出了Exception的其他子类(如IOException、SQLException等检查型异常),Spring 默认是不回滚的。 - 解决:在注解上显式配置回滚异常类型:
@Transactional(rollbackFor = Exception.class)。强烈建议在所有事务注解上加上这个配置。
四、 事务传播行为与配置问题
6. 错误的事务传播行为(Propagation)
- 场景:配置了不支持事务的传播机制。
- 原因:如果
@Transactional注解的propagation属性设置为了:Propagation.NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则把当前事务挂起。Propagation.NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。Propagation.SUPPORTS:如果当前没有事务,就以非事务方式执行。
那么在发生异常时,自然没有事务可以回滚。
五、 多线程与底层问题
7. 多线程调用
- 场景:在加了
@Transactional的方法中,开启了新的线程去执行数据库操作,新线程发生异常。 - 原因:Spring 的事务管理是基于
ThreadLocal的。每个线程拥有自己的数据库连接和事务上下文。主线程开启的事务,子线程是感知不到的。子线程抛出异常,只能回滚子线程内部(如果有的话)的操作,无法回滚主线程的操作;反之亦然。
8. 数据库引擎本身不支持事务
- 原因:Spring 事务最终是交由数据库执行的。如果你使用的数据库引擎不支持事务(例如 MySQL 的
MyISAM引擎),那么无论代码怎么写,事务都不会生效。 - 解决:将数据库引擎改为支持事务的引擎(如 MySQL 的
InnoDB)。
9. 所在的类没有交由 Spring 管理(未注入 Bean)
- 原因:如果类没有标注
@Service、@Component等注解,没有被 Spring 容器扫描成 Bean,Spring 无法为其创建代理对象,注解自然无效。
💡 总结排查指南
如果你的事务没有回滚,请按照以下步骤快速排查:
- 看引擎:数据库引擎是不是 InnoDB?
- 看 Bean:这个类有没有加
@Service注入 Spring? - 看方法:方法是不是
public的?有没有被final修饰? - 看调用:是不是发生了同一个类内部的
this调用? - 看异常:有没有配置
rollbackFor = Exception.class? - 看 Catch:异常是不是被
try-catch吃掉并且没有抛出?