这确实是 Spring 事务中最烧脑、但也是设计最精妙的部分。
事务传播行为(Propagation Behavior) 解决的核心问题是:
当一个事务方法 A,调用了另一个事务方法 B 时,方法 B 应该怎么运行?
是 B 加入 A 的事务?还是 B 自己新开一个事务?还是 B 根本就不想要事务?
Spring 提供了 7 种 传播行为,但你只需要重点掌握 最常用的 3 种。为了让你彻底理解,我用**"老板派活"**来打比方。
场景设定
-
方法 A (外部) :
ServiceA.methodA()------ 比如"下订单"。 -
方法 B (内部) :
ServiceB.methodB()------ 比如"扣减库存" 或 "记录日志"。 -
调用关系:A 调用 B。
1. REQUIRED (默认值,最常用)
口号:"有福同享,有难同当"
-
含义:如果 A 已经有事务,B 就加入 A 的事务(合成一个大事务);如果 A 没有事务,B 就自己开一个新事务。
-
比喻:老板(A)在做一个大项目,让你(B)来帮忙。如果你俩是一伙的,项目做砸了(异常),大家一起完蛋。
实战推演:
java
// ServiceA
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
serviceB.methodB(); // 调用 B
// ... A 自己的其他操作
}
// ServiceB
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// ... B 的操作
}
-
情况 1(B 报错) :B 抛出异常 -> A 和 B 都会回滚。因为他们是同一条船上的(同一个事务)。
-
情况 2(A 报错) :A 在调用完 B 之后报错 -> A 和 B 都会回滚。就算 B 已经跑完了代码,但只要大事务提交失败,B 做的事也要吐出来。
2. REQUIRES_NEW (常用作"隔离")
口号:"各过各的,互不拖累"
-
含义 :不管 A 有没有事务,B 永远开启一个新的独立事务 。如果 A 有事务,就把 A 的事务挂起(Suspend),等 B 的新事务跑完提交了,再恢复 A 的事务。
-
比喻 :老板(A)在开会,突然想喝咖啡,让你(B)去买。你买咖啡这个动作(B)和老板开会(A)是两回事。就算老板开会开崩了(A 回滚),你的咖啡已经买好了(B 提交),不用退回去。
典型场景 :"记录日志"。无论业务逻辑(下订单)成功还是失败,我都要把日志记录进数据库,不能因为订单失败回滚,把我的报错日志也回滚没了。
实战推演:
java
// ServiceA (下订单)
@Transactional(propagation = Propagation.REQUIRED)
public void checkout() {
// 1. 尝试下单
orderMapper.insertOrder();
try {
// 2. 记录日志 (不管下单成不成功,日志必须入库)
logService.saveLog();
} catch (Exception e) {
// 这里的 try-catch 很重要!
// 如果 B 报错,A 捕获后可以选择不回滚
}
// 3. 模拟异常
int i = 1 / 0;
}
// LogService (日志)
@Transactional(propagation = Propagation.REQUIRES_NEW) // <--- 重点
public void saveLog() {
logMapper.insert();
}
-
结果 :订单(A)回滚了,但是日志(B)保存成功了。
-
注意 :如果 B 报错了,A 必须
catch住 B 的异常,否则 A 也会跟着回滚。
3. NESTED (嵌套事务,较少用但很高级)
口号:"留得青山在,不怕没柴烧"
-
含义 :如果 A 有事务,B 就在 A 里面创建一个子事务(Savepoint/保存点)。
-
区别:
-
与
REQUIRED区别:B 报错回滚,只回滚 B 自己的部分 ,不会带着 A 一起死(前提是 Acatch住了 B)。 -
与
REQUIRES_NEW区别:B 虽然是独立的逻辑,但B 还没提交 。如果 A 最后挂了,B 也要陪葬(回滚)。
-
-
比喻:你在打游戏(A),打到 Boss 关前存了个档(B)。
-
如果你打 Boss 输了(B 报错),你只需要读取存档(回滚 B),不用从游戏最开始重玩(A 不回滚)。
-
但是,如果你的电脑突然断电了(A 报错/崩溃),那你的存档(B)也没了。
-
总结一张表 (面试/工作速查)
| 传播行为 | A 有事务时 B 的行为 | A 挂了,B 怎么办? | B 挂了,A 怎么办? | 典型应用场景 |
|---|---|---|---|---|
| REQUIRED (默认) | 加入 A | B 回滚 | A 回滚 | 绝大多数业务逻辑 (90%) |
| REQUIRES_NEW | 挂起 A,B 开新事务 | B 不回滚 (早已提交) | A 可选择不回滚 (需 try-catch) | 审计日志、发送流水 |
| NESTED | 在 A 中建保存点 | B 回滚 (陪葬) | A 可选择不回滚 (回滚到保存点) | 复杂业务流中尝试性的步骤 |
SUPPORTS |
加入 A | B 回滚 | A 回滚 | 只读查询 |
NOT_SUPPORTED |
挂起 A,B 以非事务运行 | / | / | 发送短信/邮件等耗时且非库操作 |
避坑指南
-
REQUIRES_NEW的死锁风险:-
如果 A 更新了表 X 的行 1,然后调用 B。
-
B(新事务)也去更新表 X 的行 1。
-
结果 :B 等着 A 释放锁,A 等着 B 执行完。死锁!
-
建议 :使用
REQUIRES_NEW时,尽量操作不同的表。
-
-
NESTED需要数据库支持:- 它依赖 JDBC 的 Savepoint 机制。大部分主流数据库(MySQL, PostgreSQL)都支持,但太老旧的可能不支持。