Spring 事务传播与常见失效场景

给方法加上 @Transactional 并不意味着所有调用都会自动处于事务中。注解最终要由 Spring 的事务基础设施识别,并在方法调用边界开始、提交或回滚事务。理解代理和事务管理器,才能定位“注解写了却没有回滚”的问题。

1. 声明式事务的基本过程

在常见的命令式 Spring 应用中,事务由 PlatformTransactionManager 管理。事务注解提供事务元数据,Spring AOP 在调用经过代理时执行事务拦截逻辑:获取或加入事务,调用目标方法,根据返回或异常情况提交或回滚。

1
调用方 → Spring 代理 → 开启或加入事务 → 目标方法 → 提交或回滚

事务通常放在 Service 的业务操作边界上,因为这里最容易表达“一组数据库修改应该一起成功或失败”。

2. 自调用为什么会绕过事务

默认代理模式只拦截经过代理的外部调用。同一个对象内部用 this.otherMethod() 调用另一个标注了 @Transactional 的方法时,调用没有经过代理,内层注解可能不会生效。

优先把不同事务边界拆到独立 Spring Bean 中,通过注入后的代理调用;也可以重新设计事务边界。不要为了绕过问题随意使用 AopContext.currentProxy(),它会增加代码耦合。

3. 回滚规则

默认规则下,未处理的运行时异常和 Error 通常触发回滚;受检异常默认不一定回滚。若业务要求某类受检异常也回滚,应显式声明规则,例如:

1
2
3
4
@Transactional(rollbackFor = Exception.class)
public void placeOrder() throws Exception {
// 写入订单、扣减库存等数据库操作
}

如果异常被方法内部捕获并吞掉,事务拦截器可能看不到异常,自然也就不会按预期回滚。应明确异常如何传播,并避免用不透明的通用异常掩盖失败原因。

4. 常见传播行为

  • REQUIRED:默认值;有当前事务就加入,没有就新建。
  • REQUIRES_NEW:挂起当前事务并开启独立事务,内外事务可分别提交或回滚;需要额外连接资源。
  • NESTED:在支持的事务管理器和资源下创建嵌套保存点;不能假设所有平台都支持。
  • SUPPORTS、MANDATORY、NOT_SUPPORTED 等选项表达其他事务存在条件,应按语义使用。

传播行为不是“异常处理开关”。选错传播方式可能让部分业务先于外层事务提交,产生需要补偿的状态。

5. 数据库事务不覆盖远程副作用

Spring 数据库事务不能自动回滚已经发出的 HTTP 请求、Redis 写入或消息发送。比如数据库事务还未提交时先调用远程服务,稍后数据库回滚,远程操作不会跟着撤销。

通常应缩短数据库事务,不要在事务内等待慢网络请求。跨服务一致性可考虑事务消息、Outbox、幂等消费和补偿流程;这些方案提供的是工程上的可靠性策略,不等于全局 ACID 事务。

6. 排查清单

  1. 调用是否经过 Spring 代理?是否存在自调用?
  2. 目标对象是否由 Spring 管理,事务管理器是否配置正确?
  3. 异常是否向外抛出,异常类型是否符合回滚规则?
  4. 多个事务方法的传播行为是否符合预期?
  5. 失败后检查数据库状态,而不只是看日志中有没有异常。

参考:Spring 声明式事务实现、@Transactional 注解模式。