Spring 的事务传播机制有哪些?
结论先行:传播行为(propagation)定义"一个事务方法被另一个事务方法调用时,事务边界怎么处理",共 7 种。真正需要理解的是三组语义:加入(REQUIRED、SUPPORTS、MANDATORY)、挂起另开(REQUIRES_NEW、NOT_SUPPORTED、NEVER)、嵌套保存点(NESTED)。生产上 90% 的选择是 REQUIRED 与 REQUIRES_NEW,NESTED 只在"内层失败不影响外层已做操作"时才用。
一、七种行为与内外层效果
| 传播行为 | 外层有事务 | 外层无事务 | 内层回滚是否影响外层 |
|---|---|---|---|
| REQUIRED(默认) | 加入 | 新建 | 是,同一事务一起回滚 |
| SUPPORTS | 加入 | 非事务执行 | 有则同回滚,无则各自独立 |
| MANDATORY | 加入 | 抛异常 | 是 |
| REQUIRES_NEW | 挂起外层、新建 | 新建 | 否,内层独立提交/回滚 |
| NOT_SUPPORTED | 挂起外层、非事务执行 | 非事务执行 | 否,内层无事务可回滚 |
| NEVER | 抛异常 | 非事务执行 | 否 |
| NESTED | 创建保存点 | 新建(等同 REQUIRED) | 部分:内层回滚到保存点,外层可继续 |
二、执行模型
调用链:Controller → OrderService.place() [REQUIRED] → LogService.write() [REQUIRES_NEW]
事务管理器视角:
place() 开始 ──▶ 事务 T1(连接 C1)
write() ──▶ 挂起 T1,开启 T2(连接 C2)
提交/回滚 T2,释放 C2
place() 继续,恢复 T1
提交/回滚 T1,释放 C1
风险:同一线程同时占用 2 个连接;连接池 10、并发 10 时极易互相等待
三、代码:三种典型组合
@Service
public class OrderService {
@Autowired
private LogService logService;
// 主流程:默认 REQUIRED
@Transactional(rollbackFor = Exception.class)
public void place(Order order) {
orderMapper.insert(order);
try {
// 日志必须落库,即使主流程回滚
logService.write(order.getId(), "下单");
} catch (Exception e) {
// 日志失败不影响主流程,异常在此吞掉
}
payService.charge(order); // 失败则整体回滚
}
}
@Service
public class LogService {
// 独立事务:主流程回滚不影响日志
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void write(Long orderId, String action) {
logMapper.insert(new OpLog(orderId, action));
}
// 嵌套事务:内层失败只回滚到保存点,外层可选择继续
@Transactional(propagation = Propagation.NESTED)
public void appendDetail(Long orderId, String detail) {
detailMapper.insert(orderId, detail);
}
}
四、四个容易答错的细节
- 挂起不是结束:REQUIRES_NEW 会把外层事务"挂起"(
TransactionSynchronizationManager解绑资源),内层结束后恢复,所以外层仍可提交。 - 内层抛异常不一定回滚外层:REQUIRES_NEW 的内层异常如果没有被外层捕获,会继续向外传播,外层同样回滚;只有外层 catch 住,才体现"内层独立"。
- NESTED 依赖保存点:底层是
Savepoint,JDBC 驱动与数据库需支持(MySQL InnoDB 支持)。内层抛异常时 Spring 回滚到保存点并释放,外层事务仍处于活动状态。 - 同类自调用:
this.logService.write()若发生在同一 Bean 内(this.write()),传播行为完全不生效——因为没走代理。
五、常见追问
- 问:REQUIRES_NEW 和 NESTED 怎么选? 需要"内层提交后即使外层回滚也保留"→ REQUIRES_NEW;需要"内层失败只撤销内层、外层继续"→ NESTED。前者是两个事务,后者是一个事务内的保存点。
- 问:传播行为能用在
@Transactional以外吗? 不能,它属于事务属性,随@Transactional生效。 - 问:
MANDATORY有什么实际用途? 强制"必须在事务中调用",常用于内部方法防御,避免被误用于无事务上下文。 - 问:异步线程里的
@Transactional会加入当前事务吗? 不会。事务绑定在ThreadLocal,异步线程是全新事务上下文,需要独立开启。 - 问:只读事务
readOnly=true有什么好处? 告诉驱动/数据库优化(如 MySQL 可走只读事务、减少 undo),并阻止误写;但它不能替代业务校验。
小结
七种传播行为按"加入 / 挂起 / 保存点"三类记最快。答题时给表格 + 一句"默认 REQUIRED,跨事务独立用 REQUIRES_NEW,局部回滚用 NESTED",再补连接池与自调用两个坑,就是可落地的答案。