Spring 事务失效的常见场景有哪些?

5 阅读8分钟

Spring 事务失效,是面试里非常高频的问题。核心要先记住一句话:

Spring 事务本质是基于 AOP 代理实现的。
事务失效,往往不是“事务功能坏了”,而是没有经过代理,或者回滚条件没满足

我按常见场景给你系统整理。


一、同类内部调用,导致事务失效

这是最经典、最常考的场景。

场景

同一个类里面,一个方法直接调用另一个加了 @Transactional 的方法:

@Service
public class OrderService {

    public void createOrder() {
        saveOrder(); // 直接调用
    }

    @Transactional
    public void saveOrder() {
        // 数据库操作
    }
}

为什么失效

Spring 事务靠代理生效。
外部调用 saveOrder() 时,调用的是代理对象,事务能织入。
但类内部 this.saveOrder() 或直接 saveOrder(),走的是当前对象本身,不经过代理,所以事务不会生效。

解决方式

  • 把事务方法拆到另一个 Spring Bean 中
  • 或通过代理对象调用自己
  • 或使用 AopContext.currentProxy() 获取代理对象再调

二、方法不是 public,事务可能失效

场景

@Transactional
private void save() {
}

或者:

@Transactional
protected void save() {
}

为什么失效

Spring 默认基于代理实现事务,通常只对 public 方法进行代理增强。
如果方法是 privateprotected,很多情况下事务拦截不到。

尤其是 private 方法,子类都无法重写,CGLIB 也没法增强。

结论

事务方法尽量写成 public


三、方法被 final 修饰,可能导致事务失效

场景

@Transactional
public final void saveOrder() {
}

为什么失效

如果 Spring 使用的是 CGLIB 代理,它是通过生成子类、重写方法来实现增强的。
final 方法不能被重写,所以事务逻辑无法织入。

补充

如果是 JDK 动态代理,代理的是接口方法,影响没那么直接;
但面试里一般回答:

final 方法会影响基于代理的事务增强,尽量不要这样写。


四、Bean 没有交给 Spring 管理

场景

OrderService orderService = new OrderService();
orderService.saveOrder();

为什么失效

@Transactional 只有在对象是 Spring 容器中的 Bean,并且通过 Spring 代理对象调用时才生效。
你自己 new 出来的对象,不归 Spring 管,自然也不会有事务代理。

结论

带事务的方法所在类,必须交给 Spring 容器管理。


五、异常被吞掉了,导致不回滚

场景

@Transactional
public void saveOrder() {
    try {
        // 数据库操作
        int i = 1 / 0;
    } catch (Exception e) {
        System.out.println("出错了");
    }
}

为什么失效

事务拦截器判断是否回滚,通常是看方法有没有把异常抛出去。
如果你把异常 catch 住了,又没有继续抛出,Spring 会认为方法正常结束,于是提交事务,而不是回滚。

解决方式

  • catch 后重新抛出异常
  • 或手动标记回滚

例如:

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

六、抛出的异常类型不对,默认不会回滚

场景

@Transactional
public void saveOrder() throws Exception {
    throw new Exception("test");
}

为什么失效

Spring 默认规则是:

  • 遇到 RuntimeExceptionError 才回滚
  • 遇到受检异常 Exception 默认不回滚

所以像:

  • IOException
  • SQLException
  • Exception

这类受检异常,如果不额外配置,事务可能不会回滚。

解决方式

@Transactional(rollbackFor = Exception.class)

面试要点

这句一定要会背:

Spring 默认只对运行时异常和 Error 回滚,对受检异常默认不回滚。


七、数据库引擎不支持事务

场景

比如 MySQL 表使用的是 MyISAM,不是 InnoDB

为什么失效

Spring 只能帮你控制事务边界,但真正提交和回滚,是数据库事务机制在做。
如果底层存储引擎本身不支持事务,那 Spring 再怎么加 @Transactional 也没用。

结论

MySQL 里要确认表引擎是 InnoDB


八、事务传播行为设置不当

场景

比如外层方法有事务,内层方法配置了不合适的传播行为:

@Transactional
public void methodA() {
    methodB();
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodB() {
}

为什么失效

不同传播行为决定方法如何加入或新建事务。
如果传播行为设置不当,可能导致你以为它在事务里执行,实际上没有。

常见传播行为影响:

  • REQUIRED:有事务就加入,没有就新建
  • REQUIRES_NEW:挂起当前事务,自己新开事务
  • SUPPORTS:有事务就加入,没有就非事务执行
  • NOT_SUPPORTED:不支持事务,始终非事务执行
  • NEVER:如果当前有事务就报错

面试点

很多“事务没按预期回滚”,其实是 传播机制理解错了


九、多线程场景下事务失效

场景

@Transactional
public void saveOrder() {
    new Thread(() -> {
        // 数据库操作
    }).start();
}

为什么失效

Spring 事务通常是绑定在线程上的,底层通过 ThreadLocal 管理当前事务资源。
你在新线程里执行数据库操作,这个线程拿不到原线程的事务上下文,所以事务不会继承过去。

结论

Spring 事务默认不能跨线程传播。


十、@Transactional 加在 static 方法上失效

为什么失效

Spring AOP 代理的是对象方法,而 static 方法属于类,不属于对象实例,无法被代理增强。

结论

事务不要加在 static 方法上。


十一、@Transactional 加在接口、实现类位置不当,可能不生效

这个场景要看具体代理方式和配置,但面试里可以这么答:

  • 一般建议把 @Transactional 写在实现类上
  • 不要过度依赖写在接口上
  • 尤其在复杂代理场景下,可能出现识别不到的问题

这样回答更稳。


十二、手动调用后置初始化或绕过代理链

比如某些特殊场景里,你拿到的不是 Spring 容器中的代理对象,而是目标对象本身,或者通过某种方式绕过了代理链,这也会导致事务失效。

本质上还是同一个原因:

没有经过 Spring 事务代理。


十三、只读事务里执行写操作,可能表现异常

场景

@Transactional(readOnly = true)
public void saveOrder() {
    // insert/update/delete
}

为什么会出问题

readOnly = true 主要是告诉框架/数据库这是只读事务。
不同数据库和 ORM 实现下,可能会:

  • 不报错但性能/行为异常
  • flush 机制异常
  • 写入不符合预期

这不一定叫“事务完全失效”,但属于事务配置不当导致的常见坑。


十四、异常发生的位置不在事务拦截范围内

比如事务方法已经执行完并提交了,后面别的逻辑再抛异常,自然无法回滚已经提交的事务。

所以要注意:

只有事务方法执行期间抛出的、并被事务拦截器感知到的异常,才会触发回滚。


十五、使用了错误的数据源或事务管理器

在多数据源项目里,经常有这种问题:

  • 业务操作走的是数据源 A
  • 配置的事务管理器却管理的是数据源 B

结果看起来加了事务,实际上没有管到真正执行 SQL 的连接。

这个在实际项目里很常见,特别是:

  • 多数据源
  • 读写分离
  • 分库分表
  • 多事务管理器并存

十六、@Transactional 标注在非 Spring 托管调用链之外的方法上

比如一个事务方法被 private、内部调用、工具类调用、异步回调等方式绕开代理,本质都是:

  • 注解写了
  • 但执行时没走代理

所以你可以把很多失效场景归纳成两大类:

  1. 事务代理没有生效
  2. 回滚规则没有生效

十七、最常见的几个失效原因,面试优先说这些

  1. 同类内部调用,绕过代理
  2. 方法不是 public
  3. 异常被捕获吞掉,没有抛出
  4. 抛出的是受检异常,默认不回滚
  5. Bean 不是 Spring 容器管理的
  6. 多线程场景,事务不能跨线程传播
  7. 传播行为配置不当
  8. 数据库本身不支持事务
  9. final/static/private 等导致代理失效
  10. 多数据源下事务管理器配错

十八、背诵版

Spring 事务失效的常见场景,本质上主要分为两类:一类是没有经过 Spring 的事务代理,另一类是虽然进了事务,但没有满足回滚条件。最典型的情况包括:同类内部方法调用导致绕过代理;事务方法不是 public;方法被 final、static、private 修饰,导致无法代理;Bean 不是 Spring 容器管理;异常被 catch 住没有继续抛出;抛出的是受检异常而不是运行时异常,默认不会回滚;事务传播行为配置不当;多线程场景下事务无法跨线程传播;数据库引擎本身不支持事务;以及多数据源场景下事务管理器配置错误。归根到底,Spring 事务依赖 AOP 代理和正确的回滚规则,只要代理链断了,或者异常没有按事务规则传递出去,事务就可能失效。


十九、万能总结

Spring 事务失效,最核心的原因有两个:第一,没有走到 Spring 的代理对象,比如同类内部调用、方法不可代理、对象不是 Spring 管理;第二,虽然进了事务,但没有触发回滚,比如异常被吞掉、抛出的是受检异常、传播行为配置不当等。