Spring 事务传播机制完全指南
从自调用失效到 7 种传播行为,配 8 张图把事务底层讲透
一文速览: 本文围绕 Spring 事务的两大核心 — 传播行为(Propagation) 与 自调用失效陷阱 展开。先讲清楚事务三大接口职责,再逐个拆解 7 种 Propagation 的执行流程,最后聚焦"为什么 @Transactional 加了等于没加"的几个常见坑。

一、为什么 Spring 事务总让人头大?
很多同学第一次用 @Transactional 都很顺 — 加注解,方法跑在事务里,异常就回滚。一旦遇到嵌套调用 、跨服务调用 、或者不同传播行为混用 ,就开始懵:为啥日志显示事务开启了,数据库数据却没回滚?
根因往往不在数据库,而在于你误以为事务注解一定生效 。本文会帮你把链路彻底打通,先讲三大核心接口的分工。
二、Spring 事务三大核心接口
Spring 事务抽象层由三个接口撑起来,每个职责单一,组合起来才完成一次事务声明周期。
1. PlatformTransactionManager — 平台无关的事务管理器
核心抽象,屏蔽 JDBC / Hibernate / JTA 等底层差异。最常用的实现是 DataSourceTransactionManager(基于 JDBC)。它定义四个关键方法:
public interface PlatformTransactionManager {
// 拿到当前事务状态(如果没有则返回 NEW 状态)
TransactionStatus getTransaction(TransactionDefinition def);
// 提交
void commit(TransactionStatus status);
// 回滚
void rollback(TransactionStatus status);
}
2. TransactionDefinition — 事务定义(配置信息)
描述一个事务的静态属性 :传播行为、隔离级别、超时时间、是否只读、回滚规则。你写 @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED, timeout = 30),最终都会解析成这个对象。
3. TransactionStatus — 事务运行时状态
描述一次具体事务的运行时信息 :是不是新事务、是否有保存点、是否完成、是否回滚。方法执行期间事务拦截器拿着这个对象判断下一步动作。

三、7 种传播行为全解
下面这张表先给你一个全局印象,后面再逐个拆开讲。
| 传播行为 | 外部有事务 | 外部无事务 | 典型场景 |
|---|---|---|---|
| REQUIRED (默认) | 加入 | 新建 | 90% 业务增删改 |
| SUPPORTS | 加入 | 不开事务 | 纯查询可选事务 |
| MANDATORY | 加入 | 抛异常 | 必须由调用方开启事务 |
| REQUIRES_NEW | 挂起外部,开新的 | 新建 | 主从分流、独立日志 |
| NOT_SUPPORTED | 挂起外部,不开 | 不开事务 | 批量导入时禁用事务 |
| NEVER | 抛异常 | 不开事务 | 不允许在事务内调用 |
| NESTED | 嵌套(Savepoint) | 新建 | 子任务失败不影响主 |
记忆口诀: "需"求新(REQUIRED / REQUIRES_NEW)、支持不支持(SUPPORTS / NOT_SUPPORTED)、强制从不(MANDATORY / NEVER)、嵌套(NESTED)。
四、REQUIRED:你写 90% 的事务都是它
行为: 最常用也是默认。若外部已有事务就加入;否则自己开一个新的。加入意味着用同一个 Connection,所有操作在同一个事务边界 里 — 一起提交、一起回滚。

实战注意
由于 REQUIRED 用同一事务,doInner 抛异常会导致整个 doOuter 回滚 ,即使 doOuter 自己 try-catch 了也没用 — 异常已被事务管理器标记 rollback-only。
@Service
public class OrderService {
@Autowired private StockService stockService;
@Transactional
public void placeOrder(Order o) {
saveOrder(o); // 1. 插入订单
try {
stockService.deduct(o); // 2. 扣库存(内部抛异常)
} catch (Exception e) {
log.warn(\"扣库存失败\");
}
// 关键:上面 catch 救不了你,事务已经被标记 rollback-only
// 走到这里 commit 时会抛 UnexpectedRollbackException
}
}
踩坑警告: try-catch 子方法异常后还想继续提交?在 REQUIRED 下几乎不可能 。要么改 REQUIRES_NEW,要么在子方法里自己吞掉(不重新抛出,不算"业务异常"),要么用 NESTED 加保存点。
五、REQUIRES_NEW:彻底独立的"小金库"
行为: 无论外部有没有事务,都 开一个新事务,并把外部事务挂起 (保存原事务状态,用另一条 Connection)。等当前方法提交/回滚完,再恢复外部事务。
这是实现"主流程失败不影响日志落地""主流程要回滚子任务必须落"这类需求的关键开关。

实战:日志一定落地
@Service
public class OrderService {
@Autowired private AuditLogService logService;
@Transactional
public void placeOrder(Order o) {
try {
saveOrder(o);
} catch (Exception e) {
logService.auditFail(o.getId(), e.getMessage()); // REQUIRES_NEW
throw e;
}
}
}
@Service
public class AuditLogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void auditFail(String orderId, String reason) { /* ... */ }
}
即使 placeOrder 整体回滚,auditFail 已经提交进数据库 ,靠 REQUIRES_NEW 隔离出来的那条独立 Connection 完成提交。
性能提示: REQUIRES_NEW 要切换 Connection、释放/获取锁,性能比 REQUIRED 低一个量级 。一次请求里循环调用 REQUIRES_NEW 方法要慎重 — 连接池很容易被打满。
六、NESTED:保存点级别的"局部回滚"
行为: 外部有事务时,开一个嵌套事务 (JDBC Savepoint)。外部没有事务时退化成 REQUIRED。区别于 REQUIRES_NEW:
- NESTED 是同一连接 上的保存点,提交时跟外层一起提交;
- REQUIRES_NEW 是不同连接 ,独立提交。

典型场景:批量导入 100 行数据,某一行报错时跳过该行,整体继续;每行就是一个 NESTED。注意 它依赖数据库支持 SAVEPOINT(MySQL InnoDB 默认支持)。
七、致命陷阱:自调用失效
讲传播行为至此,最常被忽视的最致命陷阱 必须单独说 — 同一个类内的方法自调用 ,@Transactional 会完全失效 。
金科玉律:@Transactional 的本质是通过 AOP 代理调用 。同类内 this.xxx() 直接调用绕过代理 ,注解等于不存在。
@Service
public class OrderService {
public void placeOrder(Order o) {
checkStock(o); // ❌ 失效!this 调用走不到代理
saveOrder(o);
}
@Transactional
public void checkStock(Order o) { /* 库存校验 */ }
}
上面的 checkStock 不会执行在事务里,因为 this 引用没有走 CGLIB / JDK 代理对象。要解决这类问题,三种标准方案:

解决方案 1:注入自身代理(推荐)
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入的是代理对象
public void placeOrder(Order o) {
self.checkStock(o); // ✅ 走代理
saveOrder(o);
}
@Transactional
public void checkStock(Order o) { /* ... */ }
}
注意 self 不能用 this,要靠 Spring 注入进来。注入时通过 @Lazy 避免循环依赖。
解决方案 2:暴露代理(AopContext)
@EnableAspectJAutoProxy(exposeProxy = true) // 全局开启
@Service
public class OrderService {
public void placeOrder(Order o) {
OrderService self = (OrderService) AopContext.currentProxy();
self.checkStock(o);
}
}
解决方案 3:拆类 — 把要事务保护的方法挪到独立 Bean
结构上更清晰,最稳妥的反模式是类里包打天下。Spring 官方也推荐按职责拆分。
面试常考点: "@Transactional 失效场景有哪些?" 标准答案至少 6 条:1) 自调用、2) 非 public 方法、3) final / static 方法、4) 未被 Spring 管理的类(new 出来的)、5) 异常被 catch 吃掉、6) 抛错类型不在 rollbackFor 范围内。
八、rollbackFor:默认只回滚 RuntimeException
Spring 默认 只对 RuntimeException / Error 回滚 ,遇到 checked exception(IOException 等)不自动回滚。
@Transactional // 等同于 rollbackFor = Exception.class 的反义
public void importFile() throws IOException { // 编译期异常
parseFile(); // 抛 IOException
saveData();
}
上面这段在 IOException 抛出后不会回滚 ,因为 Spring 认为 checked exception 是业务可预期 的。要回滚就显式指定:
@Transactional(rollbackFor = Exception.class)
public void importFile() throws IOException { /* ... */ }
经验法则: 从方法抛出去 的所有 checked 异常,如果调用方需要回滚一致性,就显式写 rollbackFor = Exception.class,免得某个新同事加了 throw new IOException 又踩坑。
九、传播行为选型决策树

简单口诀:
- 普通增删改 →
REQUIRED(别乱加别的) - 日志/审计必须落地 →
REQUIRES_NEW - 主子任务可独立失败 →
NESTED - 纯查询 → 不加注解或
@Transactional(readOnly=true) - 绝不让别人有事务 →
NEVER
十、隔离级别 vs 传播机制:别再混
这两个概念正交 ,但经常被混淆:传播机制 管"是否加入 / 何时开新事务",隔离级别 管"事务与事务之间看见的数据"。
| 概念 | 关注点 | 作用维度 |
|---|---|---|
| 传播机制 Propagation | 新事务 vs 加入 vs 嵌套 | 事务边界 |
| 隔离级别 Isolation | 脏读/不可重复读/幻读 | 事务之间可见性 |
| 锁 Lock | 并发修改互斥 | 数据访问互斥 |
Spring 的 Isolation.READ_COMMITTED 等值会下推到 JDBC,由数据库引擎实现。MySQL InnoDB 默认 REPEATABLE_READ,与 JDBC 标准不同。跨服务调用时,每个微服务都可以设自己的隔离级别,但只在受其管理的 Connection 范围内生效。

十一、实战 checklist:上线前自检 5 条
- 🔍 所有 @Transactional 方法是不是 public? 非 public 不走代理。
- 🔍 有没有同类自调用? 有就拆类或注入 self。
- 🔍 抛出的异常是不是 RuntimeException? 否则显式
rollbackFor。 - 🔍 try-catch 后是否重新 throw? 吞了 + 不 throw = 无法回滚。
- 🔍 跨微服务调用 要走 OpenFeign / Dubbo,事务传播只在同进程内有效,不能穿透 HTTP。
再补 3 个常被忽略的自检项:
- final / static 方法: 代理无法覆盖(final 不可继承、static 属于类不属于实例),@Transactional 直接被无视。
- private 方法: 同上,无法走代理,注解完全失效。
- 未被 Spring 管理的对象: 如果业务类用
new实例化,或用final class限定,Spring 容器压根没接管,自然没事务。
一句话总结:@Transactional 只在 Spring 容器创建并经过 AOP 代理的 public 方法上生效 ,四个条件缺一不可。
十二、总结
Spring 事务传播机制表面是 7 个枚举值,本质 只有三件事:要不要新开事务、和谁一起提交、能不能局部回滚 。理解清楚这三点,再记住 REQUIRES_NEW(独立连接)与 NESTED(Savepoint)的差别,工作中 90% 的事务难题都能在一分钟内想明白。
下一篇预告:Spring Boot 启动流程与自动配置原理 — 从 SpringApplication.run() 到自动配置类的加载顺序、@Conditional 系列注解原理、为什么自己的 starter 一行注解就生效。