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


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 传播行为掌握度:面试官不仅仅想听你背出 7 种传播行为的名字,更想知道你能不能讲清楚每种行为的语义区别——谁加入谁、谁新建、谁不支持。

  2. 场景选型能力:给你一个具体业务场景(比如日志记录、异步调用、嵌套事务),你能不能选对传播行为,而不是不管三七二十一全用默认的。

  3. 底层实现理解:如果你能说出 Spring 事务传播是基于 ThreadLocal 保持同一个数据库连接来实现的,面试官会认为你不只是 "会用",而是真的理解原理。

核心答案

Spring 定义了 7 种 事务传播行为,定义在 Propagation 枚举中:

传播行为 含义 有事务时 无事务时
REQUIRED(默认) 需要事务 加入 当前事务 新建 一个事务
SUPPORTS 支持事务 加入 当前事务 以非事务方式执行
MANDATORY 强制事务 加入 当前事务 抛异常
REQUIRES_NEW 必须新事务 挂起 当前事务,新建 新建 一个事务
NOT_SUPPORTED 不支持事务 挂起 当前事务,以非事务方式执行 以非事务方式执行
NEVER 绝不用事务 抛异常 以非事务方式执行
NESTED 嵌套事务 创建 savepoint,嵌套在当前事务中 新建 一个事务

面试中掌握 REQUIREDREQUIRES_NEWNESTED 这三个就够应付 90% 的场景了,但另外四个也得知道,面试官可能会追问。

深度解析

一、REQUIRED —— 默认,最常用

REQUIRED@Transactional 的默认传播行为。逻辑很简单:有事务就加入,没有就新建

上图说明了 REQUIRED 的两种情况:

  • 场景一methodA 已经开启了事务 T1,methodB 标记为 REQUIRED,会直接加入 T1。两个方法在同一个事务里,任何一方抛异常,整个事务都会回滚。
  • 场景二methodA 没有事务,methodB 标记为 REQUIRED,会自己新建一个事务 T1 执行。
@Service
public class OrderService {

    @Autowired
    private ItemService itemService;

    @Transactional  // 默认就是 REQUIRED
    public void createOrder() {
        orderDao.save(order);          // 保存订单
        itemService.decreaseStock();   // 扣减库存(加入当前事务)
        // 任何一步失败,订单和库存都回滚
    }
}

@Service
public class ItemService {
    @Transactional  // REQUIRED:加入 createOrder 的事务
    public void decreaseStock() {
        itemDao.decrease(itemId, count);
    }
}

二、REQUIRES_NEW —— 独立新事务

REQUIRES_NEW 不管外面有没有事务,一律自己新开一个。如果当前有事务,会先把它挂起(suspend),等自己的事务完成后再恢复。

上图的流程:

  • methodA 在事务 T1 中执行,调用 methodB 时发现传播行为是 REQUIRES_NEW
  • Spring 先把 T1 挂起(暂停),然后为 methodB 开启一个全新的独立事务 T2。
  • methodB 在 T2 中执行完毕后,T2 提交或回滚。然后 Spring 恢复 T1,methodA 继续在 T1 中执行。
  • 关键点:T2 回滚不会导致 T1 回滚,反过来也一样。两个事务完全独立。

典型应用场景——日志记录

@Service
public class OrderService {

    @Autowired
    private LogService logService;

    @Transactional  // REQUIRED
    public void createOrder() {
        try {
            orderDao.save(order);
            // 业务逻辑...
        } catch (Exception e) {
            // 订单创建失败了,但日志必须记录成功
            logService.log("订单创建失败: " + e.getMessage());
        }
    }
}

@Service
public class LogService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String message) {
        // 独立事务,即使外面的 createOrder 回滚了,日志也不会丢
        logDao.save(new Log(message));
    }
}

如果 logService.log() 用的是 REQUIRED,它加入 createOrder 的事务,订单回滚时日志也跟着回滚——日志就丢了。用 REQUIRES_NEW 就不怕,日志有自己独立的事务。

三、NESTED —— 嵌套事务

NESTED 的行为是:有事务就在当前事务里创建一个 savepoint(保存点),没有事务就新建一个

上图的关键逻辑:

  • methodB 失败:只回滚到 savepoint,methodA 的操作不受影响,可以选择继续执行或整体回滚。
  • methodA 失败:整个事务 T1 回滚,methodB 已经执行的嵌套操作也跟着回滚。

这就是 NESTEDREQUIRES_NEW 的本质区别:

  • REQUIRES_NEW:两个完全独立的事务,各自回滚互不影响。
  • NESTED:嵌套在父事务中,子事务回滚不影响父事务,但父事务回滚会连带子事务一起回滚。
@Service
public class OrderService {

    @Transactional
    public void createOrder() {
        orderDao.save(order);
        try {
            pointService.addPoints();  // NESTED:加积分
        } catch (Exception e) {
            // 积分失败不影响订单,回滚到 savepoint
            log.warn("积分发放失败,稍后重试");
        }
        // 订单正常提交
    }
}

@Service
public class PointService {
    @Transactional(propagation = Propagation.NESTED)
    public void addPoints() {
        pointDao.add(userId, 100);
    }
}

四、剩下的四种

另外四种传播行为面试中偶尔会问,简单过一下:

SUPPORTS —— 随缘模式

@Transactional(propagation = Propagation.SUPPORTS)
public void query() {
    // 有事务就加入,没有也无所谓
    // 适合只读查询,不强依赖事务
}

MANDATORY —— 强制模式

@Transactional(propagation = Propagation.MANDATORY)
public void deduct() {
    // 必须在已有事务中被调用
    // 如果没人开事务就直接调用,直接抛异常
    // 适合做 "防护":确保这个方法一定在事务上下文中执行
}

NOT_SUPPORTED —— 拒绝事务

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendEmail() {
    // 有事务就挂起,以非事务方式执行
    // 适合耗时操作(发邮件、调外部 API),避免长事务占用数据库连接
}

NEVER —— 绝不用事务

@Transactional(propagation = Propagation.NEVER)
public void check() {
    // 如果当前有事务就抛异常
    // 比 NOT_SUPPORTED 更严格
}

五、7 种传播行为对比总结

上图的决策树帮你快速判断每种传播行为在有 / 无事务情况下的表现。面试时心里有这棵树,就能从容应对。

六、传播机制的底层原理

Spring 的事务传播行为是靠 ThreadLocal + 数据库连接来实现的。

核心逻辑在 AbstractPlatformTransactionManager 中:

  • 同一个线程 中的事务信息通过 ThreadLocal 保存,getTransaction() 方法根据传播行为决定是复用当前连接(加入事务)、新建连接(新事务)、还是挂起连接(NOT_SUPPORTED / REQUIRES_NEW)。
  • 挂起(suspend):把当前事务的连接从 ThreadLocal 中取出来暂存,等新事务完成后再恢复(resume)回去。
  • 嵌套(savepoint)NESTED 利用数据库的 SAVEPOINT 机制,在当前连接上设置一个回滚点,子事务回滚时只回到这个点。

这也解释了为什么 多线程场景下事务传播会失效——ThreadLocal 是线程隔离的,新线程拿不到父线程的事务上下文。

面试高频追问

  1. NESTEDREQUIRES_NEW 有什么区别?

    REQUIRES_NEW 每次都新建一个完全独立的事务,内外事务互不影响。NESTED 是在当前事务内设置 savepoint,子事务回滚只回到 savepoint 不影响父事务,但父事务回滚会连带子事务一起回滚。简单说:REQUIRES_NEW 是 "各玩各的",NESTED 是 "父管子,子不管父"。

  2. @Transactional 默认是什么传播行为?

    REQUIRED。有事务就加入,没有就新建。90% 的场景用默认就够了。

  3. 什么场景用 REQUIRES_NEW

    日志记录、审计追踪、消息发送——这些操作不能因为主业务回滚而丢失。比如订单创建失败,但失败日志必须留下来。

  4. 事务传播在多线程下还有效吗?

    无效。Spring 事务上下文基于 ThreadLocal,子线程拿不到父线程的事务。需要在子线程中单独开启事务,或者使用编程式事务(TransactionTemplate)手动控制。

常见面试变体

  • "REQUIREDREQUIRES_NEW 的区别?"
  • "NESTEDREQUIRES_NEW 怎么选?"
  • "Spring 事务的传播行为有哪些?你平时用哪几个?"
  • "@Transactional 不指定 propagation 默认是什么?"

记忆口诀

"R 加 R 新 R 挂 S 随 M 强 N 嵌 NS 挂 NV 拒"——REQUIRED 加入、REQUIRES_NEW 新建挂起、SUPPORTS 随缘、MANDATORY 强制、NESTED 嵌套、NOT_SUPPORTED 挂起、NEVER 拒绝。

总结

Spring 事务传播机制定义了 7 种行为,核心要掌握三种:REQUIRED(默认,加入当前事务)、REQUIRES_NEW(独立新事务,适合日志等不能被主事务影响场景)、NESTED(嵌套事务,savepoint 机制,适合部分失败不影响主流程的场景)。面试时先把三种核心行为讲透,再顺带提一下另外四种,就足够拿高分了。