Spring 的事务传播机制有哪些?

#Java#面试八股

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);
    }
}

四、四个容易答错的细节

  1. 挂起不是结束:REQUIRES_NEW 会把外层事务"挂起"(TransactionSynchronizationManager 解绑资源),内层结束后恢复,所以外层仍可提交。
  2. 内层抛异常不一定回滚外层:REQUIRES_NEW 的内层异常如果没有被外层捕获,会继续向外传播,外层同样回滚;只有外层 catch 住,才体现"内层独立"。
  3. NESTED 依赖保存点:底层是 Savepoint,JDBC 驱动与数据库需支持(MySQL InnoDB 支持)。内层抛异常时 Spring 回滚到保存点并释放,外层事务仍处于活动状态。
  4. 同类自调用: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",再补连接池与自调用两个坑,就是可落地的答案。

笔记加载中…