基于本文回答
0
评论

@Transactional 注解在哪些情况下会导致事务失效(即不回滚)?

知识点图片

在 Spring 框架中,@Transactional 注解失效(即发生异常时不回滚)是开发中非常常见的问题,也是面试的高频考点。

@Transactional 的底层实现依赖于 Spring AOP(动态代理)数据库本身的事务支持。只要破坏了这两者中的任何一个环节,事务就会失效。

以下是导致 @Transactional 事务失效的常见场景,按原因分类总结:


一、 方法签名或修饰符问题

1. 方法不是 public

  • 原因:Spring AOP 默认只会对 public 方法进行拦截并生成代理。如果将 @Transactional 加在 privateprotected 或包可见(default)的方法上,Spring 不会报错,但事务根本不会生效。
  • 解决:确保加了注解的方法是 public 的。

2. 方法被 finalstatic 修饰

  • 原因:Spring AOP 通常基于 CGLIB 实现(针对没有实现接口的类)。CGLIB 是通过生成子类来创建代理对象的。final 方法不能被重写,static 方法属于类级别的调用,无法被子类拦截,因此代理机制会失效。
  • 解决:去除 finalstatic 修饰符。

二、 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))。

三、 异常处理问题

4. 异常被 catch 捕获且未抛出

  • 场景:在事务方法中使用 try-catch 捕获了异常,但没有继续往外抛出(throw)。
  • 原因:Spring AOP 只有捕获到方法抛出的异常时,才会触发回滚机制。如果异常在方法内部被吃掉了,Spring 认为方法正常执行完毕,就会执行提交(Commit)
  • 解决:如果在 catch 块中处理了逻辑,必须显式地抛出异常(如 throw new RuntimeException(e);),或者手动回滚事务:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

5. 抛出了非 RuntimeException(检查型异常)

  • 原因:Spring 默认只在遇到 RuntimeException(运行时异常)和 Error 时才会回滚。如果你抛出了 Exception 的其他子类(如 IOExceptionSQLException 等检查型异常),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 无法为其创建代理对象,注解自然无效。

💡 总结排查指南

如果你的事务没有回滚,请按照以下步骤快速排查:

  1. 看引擎:数据库引擎是不是 InnoDB?
  2. 看 Bean:这个类有没有加 @Service 注入 Spring?
  3. 看方法:方法是不是 public 的?有没有被 final 修饰?
  4. 看调用:是不是发生了同一个类内部的 this 调用?
  5. 看异常:有没有配置 rollbackFor = Exception.class
  6. 看 Catch:异常是不是被 try-catch 吃掉并且没有抛出?
右滑查看面试常问