Administrator
发布于 2022-08-02 / 1692 阅读
29

Spring 事务传播行为详解与实战选择

面试被问: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 时最大并发支付
审计用 REQUIRED120
审计用 REQUIRES_NEW2(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 的连接开销在高并发下会直接吃掉连接池,慎用,能用异步就异步。
  • 自调用导致事务注解失效,是最高频的坑。

回到那场面试,候选人后来补了一句:"我之前以为事务传播只是控制回滚范围。"其实它连数据库连接怎么分配都管——而这恰恰是压测时最容易暴露的瓶颈。

参考