Spring 事务传播行为七种解析

候选人小陈在面试拼多多时,被问到"Spring 事务的七种传播行为"。

小陈一口气背了出来:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。

面试官点点头:"好,那你告诉我,NESTED 和 REQUIRES_NEW 的区别是什么?"

小陈说:"NESTED 是嵌套事务,REQUIRES_NEW 是开启新事务。"

面试官追问:"具体怎么实现的?"

小陈支支吾吾答不上来。面试官继续:"如果外层方法没有 @Transactional,内层方法用 NESTED,会怎样?"

小陈彻底卡住。

【面试官心理】 七种传播行为,90% 的候选人能背出名字,50% 能说出一两个典型场景,但能讲清楚 NESTED 的 Savepoint 机制、以及它和 REQUIRES_NEW 的本质区别的,只有 10%。这道题是 P6 和 P7 的天然分水岭。

一、七种传播行为一览表 🔴

传播行为外层有事务外层无事务典型场景
REQUIRED加入外层事务创建新事务默认值,通用场景
SUPPORTS加入外层事务以非事务运行查询方法
MANDATORY加入外层事务抛异常必须有事务的方法
REQUIRES_NEW挂起外层事务,创建新事务创建新事务日志记录、发送消息
NOT_SUPPORTED挂起外层事务,以非事务运行以非事务运行不需要事务的分析逻辑
NEVER抛异常以非事务运行确保方法在无事务中运行
NESTED创建嵌套事务(Savepoint)创建新事务子步骤需要独立回滚

二、REQUIRED:最常用的默认值 🔴

2.1 语义

如果当前存在事务,加入该事务;如果不存在事务,创建一个新事务。

2.2 典型场景

@Service
public class TransferService {

    @Transactional
    public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
        // 外层开启事务
        accountService.deduct(fromAccount, amount);      // ✅ 加入外层事务
        accountService.add(toAccount, amount);            // ✅ 加入外层事务
        notificationService.sendNotify(toAccount);         // ✅ 加入外层事务
    }
}

@Service
public class AccountService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void deduct(String account, BigDecimal amount) {
        // 复用外层事务,任何一步失败全部回滚
        accountMapper.deduct(account, amount);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void add(String account, BigDecimal amount) {
        accountMapper.add(account, amount);
    }
}

三个方法共用同一个事务,任何一步失败,全部回滚。这是最常见的事务传播场景。

2.3 外层无事务时

@Transactional
public void outerMethod() {
    // 外层开启事务
    innerMethod(); // REQUIRES_NEW,创建新的事务
}

public void noTransactionMethod() {
    // ⚠️ 外层无事务
    innerMethod(); // REQUIRED,自动创建新事务
}

@Transactional(propagation = Propagation.REQUIRED)
public void innerMethod() {
    // REQUIRED 保证它一定在事务中运行
}

三、REQUIRES_NEW:独立事务的守护者 🔴

3.1 语义

挂起当前事务(如果有),创建新事务。内外层事务完全隔离,互不影响。

3.2 典型场景

日志记录是最经典的 REQUIRES_NEW 场景——日志的提交不应受主业务事务回滚的影响

@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        // 1. 保存订单(主业务,失败要回滚)
        orderMapper.insert(order);

        // 2. 扣减库存
        stockService.reduce(order.getSkuId(), order.getQuantity());

        // 3. 发送通知(REQUIRES_NEW,独立事务)
        //    即使主事务回滚,消息也要发出去
        messageService.sendOrderCreated(order.getId());
    }
}

@Service
public class MessageService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendOrderCreated(Long orderId) {
        // 独立事务,不受 OrderService 的事务影响
        // 如果主事务回滚,这条消息仍然会发送
        messageMapper.insert(orderId, "ORDER_CREATED");
    }
}
💡

这里有一个经典的面试陷阱:很多人以为"消息发送"用 REQUIRES_NEW 是为了"解耦性能",但真正的原因是数据一致性。如果消息发送和主业务在同一个事务里,当主事务回滚时消息也被回滚,消息队列就永远收不到这条消息。

3.3 外层无事务时

public void noTransactionOuter() {
    // ⚠️ 外层无事务
    reRequiresNewMethod(); // REQUIRES_NEW 会创建一个新的事务
}

即使外层没有事务,REQUIRES_NEW 也会创建新事务。这和 REQUIRED 不同——REQUIRED 在外层无事务时创建新事务,REQUIRES_NEW 在任何情况下都创建新事务。

四、NESTED:Savepoint 的艺术 🟡

4.1 语义

如果当前存在事务,在当前事务的嵌套级别创建一个 Savepoint(保存点);如果不存在事务,创建新事务。

这是七种传播行为中最难理解的一个,核心概念是数据库 Savepoint

4.2 Savepoint 机制详解

数据库 Savepoint(保存点)允许将一个大事务拆分成多个子阶段,每个子阶段可以部分回滚——只回滚到某个保存点,而不是全部回滚。

@Transactional
public void batchImport(List<Record> records) {
    int count = 0;
    for (Record record : records) {
        try {
            importOne(record); // NESTED,每条记录一个保存点
            count++;
        } catch (Exception e) {
            // ⚠️ 只回滚到当前保存点,不影响已成功的记录
            log.warn("第{}条记录导入失败,继续处理", count + 1);
        }
    }
    log.info("成功导入 {} 条记录", count);
}

@Transactional(propagation = Propagation.NESTED)
public void importOne(Record record) {
    // 如果这里抛异常,只回滚这一条
    // 不会影响 batchImport 中已经成功导入的其他记录
    recordMapper.insert(record);
}

4.3 NESTED vs REQUIRES_NEW:核心区别

这是 P6/P7 最重要的区分问题:

维度NESTEDREQUIRES_NEW
事务数量同一数据库连接,同一物理事务不同数据库连接,不同物理事务
回滚范围可部分回滚(Savepoint)整个事务回滚
资源占用少(同一连接)多(需要两个连接)
性能优(轻量级保存点)差(两阶段事务开销)
外层回滚影响嵌套事务一起回滚外层回滚不影响内层(已提交)
实现原理JDBC Savepointsuspend/resume 事务对象
// REQUIRES_NEW 的执行流程(伪代码)
@Transactional
public void outer() {
    // 开启事务A
    System.out.println("事务A开始");
    inner_REQUIRES_NEW(); // ⚠️ 事务A被挂起
    // 事务A被恢复(inner提交后才恢复)
    System.out.println("事务A继续");
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner_REQUIRES_NEW() {
    // 开启事务B(与事务A完全独立)
    System.out.println("事务B开始和提交");
    // 事务B提交后,事务A恢复
}

// 输出:事务A开始 → 事务B开始和提交 → 事务A继续
//      如果 inner 抛异常回滚,事务A继续执行(事务B已提交,不受影响)
// NESTED 的执行流程(伪代码)
@Transactional
public void outer() {
    // 开启事务A(也是事务B的事务,因为是嵌套)
    System.out.println("事务开始,Savepoint创建");
    inner_NESTED(); // 创建Savepoint
    // ⚠️ inner回滚到Savepoint,但外层事务不全部回滚
    System.out.println("外层继续执行");
}

@Transactional(propagation = Propagation.NESTED)
public void inner_NESTED() {
    // 与外层同一物理事务,但有Savepoint隔离
    System.out.println("嵌套事务执行");
    // 如果这里抛异常,只回滚到Savepoint
}

// 输出:事务开始,Savepoint创建 → 嵌套事务执行
//      如果 inner 抛异常,外层收到异常,但如果外层捕获了
//      外层仍然可以继续执行(因为只回滚到Savepoint)
⚠️

NESTED 的关键限制:只支持 DataSourceTransactionManager(JDBC 单数据源事务)。如果使用 JtaTransactionManager(分布式事务),NESTED 会被降级为 REQUIRED。原因很简单:分布式事务不支持 Savepoint 机制。

五、SUPPORTS、MANDATORY:查询的哲学 🟡

5.1 SUPPORTS:跟着外层走

如果当前有事务,加入事务;如果没有,以非事务方式运行。

@Transactional(propagation = Propagation.SUPPORTS)
public User getUser(Long id) {
    // 有外层事务时,加入事务(确保读一致性)
    // 无外层事务时,普通查询(允许脏读等)
    return userMapper.selectById(id);
}

5.2 MANDATORY:必须有事务

如果当前有事务,加入事务;如果没有,抛 IllegalStateException

@Transactional(propagation = Propagation.MANDATORY)
public void updateUser(User user) {
    // 确保调用者必须在事务中调用
    // 如果在非事务方法中被调用,直接抛异常
    userMapper.update(user);
}

// 使用方
@Transactional
public void serviceMethod() {
    updateUser(user); // ✅ 正常执行
}

public void nonTransactionMethod() {
    updateUser(user); // ⚠️ 抛 IllegalStateException
}

六、NOT_SUPPORTED 和 NEVER:明确拒绝事务 🟡

6.1 NOT_SUPPORTED:挂起事务

如果当前有事务,挂起它,以非事务方式运行。

@Transactional
public void importDataFromThirdParty() {
    // 主业务在事务中
    processMainData();

    // 调用第三方接口,耗时很长
    // ⚠️ 如果这个在事务中运行,会长时间占用数据库连接
    //    导致其他请求超时
    // 用 NOT_SUPPORTED 挂起事务,避免长时间占用连接
    thirdPartyService.syncData(NOT_SUPPORTED);
}

6.2 NEVER:强制非事务

如果当前有事务,抛 RemoteException;如果没有,以非事务运行。

这是一个防御性的传播行为,用于确保方法绝对不在事务中运行——通常用于防止误用。

七、外层无事务时的边界情况 🔴

这是面试官最爱的追问方向,能答出来的候选人凤毛麟角:

传播行为外层无事务时行为结果
REQUIRED创建新事务✅ 正常工作
SUPPORTS以非事务运行✅ 正常工作
MANDATORYIllegalStateException❌ 运行时异常
REQUIRES_NEW创建新事务✅ 正常工作
NOT_SUPPORTED以非事务运行✅ 正常工作
NEVER以非事务运行✅ 正常工作
NESTED创建新事务✅ 正常工作(等价于 REQUIRED)

关键发现

  • MANDATORY 是唯一一个在外层无事务时会抛异常的传播行为
  • NESTED 在外层无事务时,会降级为创建新事务(等价于 REQUIRED)

八、❌ 错误示范

候选人原话一

"NESTED 就是嵌套事务,和 REQUIRES_NEW 差不多,都是开启新事务。"

问题诊断

  • NESTED 和 REQUIRES_NEW 是完全不同的机制
  • NESTED 是同一物理事务的嵌套级别,用 Savepoint 部分回滚
  • REQUIRES_NEW 是完全独立的物理事务
  • 混淆两者的候选人,90% 没有实际用过嵌套事务

候选人原话二

"SUPPORTS 就是如果有事务就用,没有就不用,很简单。"

问题诊断

  • 只理解了字面意思,没理解"以非事务运行"意味着什么
  • 非事务运行意味着不会开启数据库事务,脏读、不可重复读都有可能发生
  • 在查询方法上用 SUPPORTS,需要开发者非常清楚隔离级别的影响

候选人原话三

"NESTED 在 JTA 分布式事务下也能工作得很好。"

问题诊断

  • JTA 不支持 Savepoint,所以 NESTED 在 JTA 环境下会被降级为 REQUIRED
  • 如果依赖 Savepoint 的部分回滚功能,在分布式环境下会完全失效

【面试官心理】 这道题我通常这样追问:NESTED 为什么在 JTA 环境下不工作?很多候选人说"因为 JTA 不支持 Savepoint",但说不出为什么 JTA 不支持,以及降级后的行为是什么。能完整回答的,基本都是 P7 候选人。

九、工程选型指南

场景推荐传播行为原因
普通增删改业务方法REQUIRED(默认)最通用,保持事务一致性
日志记录、消息发送REQUIRES_NEW独立提交,不受主事务影响
第三方接口调用(耗时)NOT_SUPPORTED避免长时间占用数据库连接
查询方法(读一致性)REQUIRED确保在事务中读取
查询方法(允许脏读)SUPPORTS性能优化
确保必须有事务的方法MANDATORY防御性校验
子步骤独立回滚NESTEDSavepoint 机制
确保无事务NEVER防御性校验