文章目录
- [一、Spring 事务传播行为是什么?](#一、Spring 事务传播行为是什么?)
- [二、Spring 提供的 7 种事务传播行为](#二、Spring 提供的 7 种事务传播行为)
- [三、为什么 Spring 默认使用 REQUIRED?](#三、为什么 Spring 默认使用 REQUIRED?)
- 四、为什么其他传播模式一般"很少用"?
- 五、为什么内部方法调用不会触发事务?
- 六、事务传播行为场景
- 七、总结
7 种事务传播行为、默认行为的原因、内部调用为什么事务失效、为什么不用其他模式。
一、Spring 事务传播行为是什么?
Spring 的事务是基于 AOP(代理) 实现的,而传播行为(Propagation Behavior)定义了:
当一个带 @Transactional 的方法被调用时,它应该如何参与已有事务?
Spring 提供 7 种事务传播机制,每种机制都有不同的语义。
二、Spring 提供的 7 种事务传播行为
| 传播行为 | 说明 | 是否新建事务 | 是否加入现有事务 | 使用频率 |
|---|---|---|---|---|
| REQUIRED(默认) | 没有事务就创建,有事务就加入 | ✔ | ✔ | ⭐⭐⭐⭐⭐ |
| REQUIRES_NEW | 开启新事务,挂起外部事务 | ✔ | ✘ | ⭐⭐⭐ |
| SUPPORTS | 有事务就加入,没有就非事务执行 | ✘ | ✔ | ⭐⭐ |
| NESTED | 外部事务中开启子事务(Savepoint) | ✔(子事务) | ✔ | ⭐⭐ |
| MANDATORY | 必须在事务中,否则报错 | ✘ | ✔ | ⭐ |
| NOT_SUPPORTED | 不用事务,挂起当前事务 | ✘ | ✘ | ⭐ |
| NEVER | 禁止事务,否则报错 | ✘ | ✘ | ⭐ |
三、为什么 Spring 默认使用 REQUIRED?
默认传播行为是:
PROPAGATION_REQUIRED
它的语义最符合业务场景:
- 如果没有事务:创建一个事务
- 如果已经有事务:加入旧事务
✔ 1. 最符合真实业务逻辑
下单例子:
saveOrder()
saveOrderItem()
reduceStock()
它们必须:
要么全部成功,要么全部回滚
------ REQUIRED 刚好满足
✔ 2. 开销小、风险低
不会频繁挂起事务(相比 REQUIRES_NEW)。
不需要额外 savepoint(相比 NESTED)。
✔ 3. 覆盖 95% 的使用场景
因此成为默认。
四、为什么其他传播模式一般"很少用"?
📌 1. REQUIRES_NEW 为啥不常用?
它会:
- 挂起外部事务
- 开启一个新的事务
成本比较大:
- 需要额外数据库连接
- 可能造成数据不一致(外部回滚,但内部提交)
适合"写日志/写流水这种必须独立提交的业务"
不是常态。
📌 2. SUPPORTS 为啥不常用?
它的语义不稳定:
- 有事务 → 加入
- 无事务 → 无事务执行
对于开发来说,行为太不可控。
出现问题时更难排查。
📌 3. MANDATORY 和 NEVER 基本不使用
因为:
- 很少有人希望"必须有事务,否则报错"(MANDATORY)
- 也很少有人希望"绝不能有事务,有就报错"(NEVER)
业务上没有这种强硬需求。
📌 4. NOT_SUPPORTED 为什么少?
它执行时会:
- 挂起事务
- 强制无事务执行
但你完全可以:
直接不加 @Transactional 就好了
没必要挂起事务,成本也大。
📌 5. NESTED 为什么很少用?
- 要求数据库/JDBC 支持 Savepoint
- 很多人误以为它 = REQUIRES_NEW(这是误区)
- 使用场景更少
一般业务用不到。
五、为什么内部方法调用不会触发事务?
Spring 的事务通过 代理对象 实现:
外部调用 → Proxy → 事务增强 → 真正方法
但是内部调用:
A() 内部调用 B()
this.B() // 不经过 Proxy!
Spring 完全检测不到这次调用,因此:
内部调用不会触发事务,也不会应用传播行为。
✔ 正确做法:
方案 1:将方法拆分到不同 Service
让 A 调 B 时经过代理:
java
@Autowired
BService b;
public void A() {
b.B(); // 通过代理对象调用
}
方案 2:强制获取自身代理调用
java
AService proxy = (AService) AopContext.currentProxy();
proxy.B();
六、事务传播行为场景
如果 A 调用了 B,并且 A 有事务,那么 B 会加入到 A 的事务中,因为 REQUIRED 会在已有事务的上下文中执行方法。
如果 A 没有事务,B 的传播行为是 REQUIRED,Spring 会为 B 创建一个新的事务。
场景 1:无事务 → 调 A(REQUIRED)
[无事务]
↓
调用 A()
↓
Spring:创建 Tx1
场景 2:A 有事务调用 B(REQUIRED)
Tx1 已存在
↓
A() 调用 B()
↓
B 加入 Tx1
最终:
一个事务:Tx1
A → B → C
七、总结
✔ 内部方法调用不会触发事务(绕过代理)
✔ private 方法、不在同一个 Bean 中也不会触发事务
✔ 异常没抛出、不符合回滚规则 → 不会回滚
✔ Proxy 方式不对(比如 final 类)导致事务失效
Spring 默认使用 REQUIRED,因为:
"没有事务就创建,有事务就加入"
能保证整体业务的原子性
其他传播行为不常用是因为:
- 场景小众(REQUIRES_NEW)
- 行为不可控(SUPPORTS)
- 依赖底层支持(NESTED)
- 语义太强/太弱(MANDATORY、NEVER)
- 不如不用注解(NOT_SUPPORTED)
内部调用不会触发事务是因为:
Spring 需要通过代理拦截方法,但内部
this.xxx()调用绕过代理。