Spring 的事务传播行为(Propagation)

这确实是 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 一起死(前提是 A catch 住了 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 以非事务运行 / / 发送短信/邮件等耗时且非库操作

避坑指南

  1. REQUIRES_NEW 的死锁风险

    • 如果 A 更新了表 X 的行 1,然后调用 B。

    • B(新事务)也去更新表 X 的行 1。

    • 结果 :B 等着 A 释放锁,A 等着 B 执行完。死锁!

    • 建议 :使用 REQUIRES_NEW 时,尽量操作不同的表。

  2. NESTED 需要数据库支持

    • 它依赖 JDBC 的 Savepoint 机制。大部分主流数据库(MySQL, PostgreSQL)都支持,但太老旧的可能不支持。
相关推荐
梁萌1 小时前
MySQL关联查询原理与优化
数据库·mysql
没有bug.的程序员1 小时前
K8s 环境中的 JVM 调优实战
java·jvm·spring·云原生·容器·kubernetes
Trouvaille ~1 小时前
【Java篇】以简驭繁:接口的精简与程序的优雅
java·开发语言·接口·抽象工厂模式·类和对象·javase·基础入门
綝~1 小时前
Excel导入MongoDB操作手册
数据库·excel
一只乔哇噻1 小时前
java后端工程师+AI大模型开发进修ing(研一版‖day62)
java·开发语言·算法·语言模型
利刃大大1 小时前
【JavaSE】十、ArrayList && LinkedList
java·链表·数组
Qiuner1 小时前
Spring 机制六: MVC 全链路源码解析:从 DispatcherServlet 到返回值解析(超硬核源码深度)
java·spring boot·后端·spring·mvc
子一!!1 小时前
并查集(Union-Find)数据结构
java·数据结构·算法
哈库纳玛塔塔1 小时前
MongoDB 数据库 ORM/ODM 新工具
java·数据库·spring boot·mongodb·orm