面试被问:REQUIRES_NEW 到底开了几个连接
前阵子面一个五年经验的候选人,我随口问:"方法 A 用 REQUIRED 调方法 B,B 用 REQUIRES_NEW,这俩用的是同一个数据库连接吗?"对方答"应该是同一个吧,都在一个事务里"。这其实是个很常见的误解。REQUIRES_NEW 会挂起外层事务,开一个全新的物理连接。我把 Spring 的 7 种传播行为捋了一遍,结合我们支付链路的实际选择讲清楚。
七种传播行为速查
| 传播行为 | 外层无事务 | 外层有事务 |
|---|---|---|
| REQUIRED(默认) | 新建 | 加入外层 |
| SUPPORTS | 无事务运行 | 加入外层 |
| MANDATORY | 抛异常 | 加入外层 |
| REQUIRES_NEW | 新建 | 挂起外层,新建自己的 |
| NOT_SUPPORTED | 无事务运行 | 挂起外层,无事务运行 |
| NEVER | 无事务运行 | 抛异常 |
| NESTED | 新建 | 外层事务内的子事务(savepoint) |
嵌套事务:NESTED 和 REQUIRES_NEW 不是一回事
这是第二个高频混淆点。NESTED 是外层事务里的子事务,靠数据库 savepoint 实现;外层回滚,子事务一起回滚,但子事务自己回滚不影响外层继续。而 REQUIRES_NEW 是完全独立的新事务,外层提交或回滚跟它无关。
@Transactional(propagation = Propagation.NESTED)
public void logInner() { /* 失败只回滚到这里,外层可继续 */ }
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void audit() { /* 独立事务,外层回滚它也不回滚 */ }
注意 NESTED 依赖 JDBC savepoint,数据源必须支持(MySQL InnoDB 支持),且是同一连接内的逻辑子事务。
REQUIRES_NEW 的连接开销,别滥用
真正的坑在资源上。每次 REQUIRES_NEW 都要从连接池拿一个全新的物理连接,外层事务的连接被挂起(仍持有,不释放)。我们在支付链路一度滥用它记审计日志:
@Transactional
public void pay(Order o) {
deduct(o); // REQUIRED,外层连接
auditLog.write(o); // REQUIRES_NEW,又拿一个连接
}
压测时发现:一笔支付本应只占 1 个连接,却因为审计日志的 REQUIRES_NEW 占用了 2 个。连接池设的 20,高并发下连接被迅速耗尽,出现"获取连接超时"。监控数字:
| 写法 | 单笔支付占用连接 | 连接池 20 时最大并发支付 |
|---|---|---|
| 审计用 REQUIRED | 1 | 20 |
| 审计用 REQUIRES_NEW | 2(1 挂起 + 1 新) | 10 |
把审计日志改成异步发消息(而非在事务内同步写库),既解耦又省连接,并发能力直接翻倍。
我们实际怎么选
- 同库强一致写:一律 REQUIRED,别拆,拆了反而有不一致风险。
- 必须独立提交的旁路操作(如审计、记账流水):用 REQUIRES_NEW,但要清楚它多占一个连接;更优解是异步化。
- 可部分失败的子操作:用 NESTED,借助 savepoint 局部回滚。
- 绝对不能混入事务的只读查询:用 NOT_SUPPORTED 挂起外层,避免长事务拖住连接。
一个容易翻车的地方
Spring 的事务传播只有跨方法、且通过代理调用才生效。同一个类里方法 A 直接调方法 B(this.b()),B 上的 @Transactional 不会生效,这是自调用陷阱,我在 Code Review 里见过太多次。
小结
- REQUIRES_NEW 挂起外层、开新事务和新连接;NESTED 是外层内的 savepoint 子事务,二者完全不同。
- REQUIRES_NEW 的连接开销在高并发下会直接吃掉连接池,慎用,能用异步就异步。
- 自调用导致事务注解失效,是最高频的坑。
回到那场面试,候选人后来补了一句:"我之前以为事务传播只是控制回滚范围。"其实它连数据库连接怎么分配都管——而这恰恰是压测时最容易暴露的瓶颈。