事务传播行为(7种)

候选人小郑在面试字节 P6 时,面试官问道:

"Spring 事务的传播行为有哪几种?"

小郑说:"有 REQUIRED、REQUIRES_NEW...还有几个我记不清了..."

面试官追问:"REQUIRED 和 REQUIRES_NEW 的区别是什么?NESTED 呢?"

小郑说:"NESTED 是嵌套事务,REQUIRED 是加入现有事务..."

面试官:"那你说说它们分别适合什么场景?"

小郑答不上来。

【面试官心理】 这道题我用来测试候选人对 Spring 事务传播行为的深度理解。7 种传播行为中,REQUIRED、REQUIRES_NEW、NESTED 是最常用的。能说清区别的占 40%,能说清适用场景的占 20%。能说出 NESTED 的 Savepoint 机制的,基本都有实战经验。


一、核心问题 🔴

1.1 问题拆解

第一层:概念

  • "Spring 事务有哪 7 种传播行为?"
  • "什么是事务传播行为?为什么需要它?"

第二层:区别

  • "REQUIRED 和 REQUIRES_NEW 的区别是什么?"
  • "NESTED 和 REQUIRED 的区别是什么?"
  • "SUPPORTS 和 NOT_SUPPORTED 适合什么场景?"

第三层:原理

  • "Spring 是怎么实现这些传播行为的?"
  • "REQUIRES_NEW 为什么会挂起当前事务?"
  • "NESTED 的 Savepoint 是怎么工作的?"

第四层:实战

  • "什么场景下应该用 REQUIRES_NEW 而不是 REQUIRED?"
  • "NESTED 有什么限制?"
  • "嵌套事务的回滚会有什么影响?"

1.2 ❌ 错误示范

候选人原话 A:"传播行为就是事务之间的调用规则,没什么特别的。"

问题诊断

  • 知道这个概念,但不理解为什么需要
  • 说不清实际业务中的价值

候选人原话 B:"REQUIRES_NEW 就是每次都创建新事务,很简单。"

问题诊断

  • 知道结论,但不理解底层实现
  • 不知道"挂起当前事务"是什么意思
  • 不知道 NESTED 和 REQUIRES_NEW 的本质区别

候选人原话 C:"NESTED 和 REQUIRES_NEW 一样,都是新事务。"

问题诊断

  • 完全混淆了两个概念
  • NESTED 是嵌套事务(同一连接 + Savepoint),不是新事务
  • 这是面试中的高频陷阱

1.3 标准回答

P5 回答:7 种传播行为

Spring 定义了 7 种事务传播行为,核心作用是控制事务方法被调用时,如何决定是否加入现有事务或创建新事务

public enum Propagation {
    REQUIRED   ("支持当前事务,如果没有则创建新事务"),   // 默认
    REQUIRES_NEW("创建新事务,挂起当前事务"),
    SUPPORTS   ("支持当前事务,如果没有则以非事务执行"),
    NOT_SUPPORTED("不支持当前事务,挂起当前事务"),
    MANDATORY   ("必须在事务中执行,否则抛异常"),
    NEVER       ("不能在事务中执行,否则抛异常"),
    NESTED      ("在嵌套事务中执行,如果没有则创建新事务")
}

1.4 追问升级

追问 1:REQUIRED vs REQUIRES_NEW

这是最常用的对比,必须掌握:

对比维度REQUIREDREQUIRES_NEW
已有事务时加入现有事务创建新事务
没有事务时创建新事务创建新事务
回滚影响异常时整个事务回滚新事务独立回滚,不影响外层
数据库连接复用外层事务的连接使用新的连接
性能开销较低较高(需要创建新连接)
适用场景大多数业务方法日志记录、独立校验等
// REQUIRED 场景:业务数据一致性优先
@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);           // 订单表
    inventoryService.deduct(order);       // 库存表
    paymentService.process(order);         // 支付
    // 要么全部成功,要么全部回滚
}

// REQUIRES_NEW 场景:日志记录不影响主业务
@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);

    // 日志需要单独提交,即使后续失败也不影响日志
    try {
        logService.info("订单创建成功: " + order.getId());
    } catch (Exception e) {
        // 吞掉异常,不影响主流程
    }

    throw new RuntimeException("支付失败"); // 这个异常会导致上面的 orderRepository.save() 回滚
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void info(String message) {
    // 这个方法有自己的事务,独立提交
    // createOrder 的异常不会导致这个事务回滚
}
REQUIRED 执行流程:
createOrder() 开启事务 T1
  └─ orderRepository.save() 加入 T1
  └─ logService.info() 加入 T1(同一个事务)
      └─ 抛异常
      └─ 整个事务 T1 回滚
      └─ 订单和日志都回滚 ❌

REQUIRES_NEW 执行流程:
createOrder() 开启事务 T1
  └─ orderRepository.save() 加入 T1
  └─ logService.info() 挂起 T1,开启新事务 T2
      └─ 正常执行
      └─ T2 提交 ✓
  └─ 抛异常
  └─ T1 回滚
      └─ 订单回滚 ❌
      └─ 日志已提交,不受影响 ✓

追问 2:REQUIRES_NEW 的挂起机制

这是理解 REQUIRES_NEW 的关键:

// AbstractPlatformTransactionManager.getTransaction()
public final TransactionStatus getTransaction(TransactionDefinition definition) {
    Object transaction = doGetTransaction();

    // 检查是否已存在事务
    if (isExistingTransaction(transaction)) {
        // 已有事务 → 检查传播行为
        return handleExistingTransaction(transaction, definition);
    }

    // 没有事务 → 根据传播行为处理
    if (definition.getPropagationBehavior() == Propagation.REQUIRES_NEW) {
        // 【关键】挂起当前事务,创建新事务
        SuspendedResourcesHolder suspendedResources = suspend(null);
        try {
            Object newTransaction = doGetTransaction();
            // 创建新的数据库连接
            doBegin(newTransaction, definition);
            return new TransactionStatus(...);
        } finally {
            // ...
        }
    }
}

private SuspendedResourcesHolder suspend(Object transaction) {
    if (this.suspendedResources != null) {
        // 1. 暂停当前事务(数据库连接)
        // 2. 保存挂起状态到 SuspendedResourcesHolder
        // 3. 释放当前连接
    }
    return suspendedResources;
}

"挂起"本质上是:

  1. 保存当前事务的上下文(Connection、状态等)
  2. 释放当前的数据库连接
  3. 获取新的数据库连接用于新事务
  4. 新事务完成后,恢复挂起的事务

追问 3:NESTED 嵌套事务

NESTED 是最容易被误解的传播行为:

对比维度REQUIREDNESTED
回滚影响异常导致整个事务回滚可以只回滚嵌套部分
数据库连接同一连接同一连接
机制Savepoint(保存点)
回滚后整个事务结束可以继续执行
适用场景数据一致性要求高子操作需要独立回滚
// REQUIRED:异常导致全部回滚
@Transactional
public void methodA() {
    doStep1();  // 成功
    doStep2();  // 成功
    doStep3();  // 抛异常
    // 1、2、3 全部回滚
}

// NESTED:嵌套部分可以独立回滚
@Transactional
public void methodA() {
    doStep1();  // 成功
    doStep2();  // 成功
    try {
        methodB();  // NESTED
    } catch (Exception e) {
        // B 回滚,但 A 可以继续
        // 1、2 仍然提交
    }
    doStep4();  // 继续执行
}

@Transactional(propagation = Propagation.NESTED)
public void methodB() {
    // 在嵌套事务中执行
    // 可以回滚到 Savepoint,而不影响外层事务
}

NESTED 的 Savepoint 机制:

// JDBC 实现(伪代码)
Connection conn = getConnection();
conn.setAutoCommit(false);

// 创建 Savepoint
Savepoint savepoint = conn.setSavepoint("methodB_start");

try {
    methodBLogic();
    conn.releaseSavepoint(savepoint);
} catch (Exception e) {
    // 回滚到 Savepoint
    conn.rollback(savepoint);
    // 继续执行 methodA 的后续逻辑
}
⚠️

NESTED 依赖于数据库的 Savepoint 机制。如果数据库不支持 Savepoint(如某些配置下的 MySQL),NESTED 会退化到 REQUIRED。Oracle 和 SQL Server 完全支持 Savepoint。

追问 4:其他传播行为

// SUPPORTS:支持但不强制
@Transactional(propagation = Propagation.SUPPORTS)
public void query() {
    // 如果有外层事务,加入
    // 如果没有外层事务,以非事务方式执行
    // 适合:查询方法(不需要事务,但调用者可能有)
}

// NOT_SUPPORTED:挂起当前事务
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void batchImport() {
    // 挂起外层事务,以非事务方式执行
    // 适合:大数据量导入(事务会影响性能)
}

// MANDATORY:强制在事务中
@Transactional(propagation = Propagation.MANDATORY)
public void mustInTransaction() {
    // 如果没有外层事务,抛 IllegalStateException
    // 适合:某些核心方法必须由调用者在事务中调用
}

// NEVER:强制不在事务中
@Transactional(propagation = Propagation.NEVER)
public void mustNotInTransaction() {
    // 如果有外层事务,抛 IllegalStateException
    // 适合:只读方法、某些不允许在事务中执行的场景
}

二、延伸问题 🟡

2.1 传播行为的常见坑

坑1:REQUIRES_NEW 和事务同步问题

@Transactional
public void methodA() {
    // 在 REQUIRES_NEW 之前修改的数据
    dataRepository.update(1); // 修改了数据

    // REQUIRES_NEW 方法可以看到这个修改吗?
    // 取决于数据库隔离级别:
    // - READ_COMMITTED: 可以看到
    // - READ_UNCOMMITTED: 可以看到
    // - SERIALIZABLE: 取决于具体实现
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // 这里可以看到 methodA 对数据库的修改吗?
}

坑2:NESTED 回滚后的继续执行

@Transactional
public void methodA() {
    doStep1();
    try {
        methodB(); // NESTED
    } catch (Exception e) {
        // B 回滚到 Savepoint
        // A 可以继续执行
        log.warn("B 回滚了,但 A 继续");
    }
    doStep3(); // 继续执行
}

@Transactional(propagation = Propagation.NESTED)
public void methodB() {
    // NESTED 的 Savepoint 回滚后
    // B 后续的逻辑还可以继续执行吗?
    // —— 可以!但 B 对数据库的修改已经回滚了
}

2.2 默认传播行为

// Spring 的默认传播行为是 REQUIRED
// 但要注意:有些 ORM 框架可能有不同的默认行为

// 设置默认传播行为
@Transactional(propagation = Propagation.REQUIRED) // 这是默认值,可以不写

// 也可以在事务管理器级别配置
@Bean
public DataSourceTransactionManager transactionManager() {
    DataSourceTransactionManager tm = new DataSourceTransactionManager();
    tm.setDefaultTimeout(30);
    tm.setValidateExistingTransaction(true);
    return tm;
}

三、生产避坑

3.1 误用 REQUIRES_NEW 导致连接耗尽

// ❌ 错误:在循环中调用 REQUIRES_NEW 方法
@Transactional
public void processItems(List<Item> items) {
    for (Item item : items) {
        itemProcessor.process(item); // REQUIRES_NEW
        // 每次调用都会创建一个新连接
        // 如果 items 有 10000 个,数据库连接池可能被打满
    }
}

// ✅ 正确做法
@Transactional
public void processItems(List<Item> items) {
    for (Item item : items) {
        try {
            itemProcessor.process(item); // REQUIRES_NEW
        } catch (Exception e) {
            log.error("处理失败: " + item.getId(), e);
        }
    }
}

// 或者用批量处理代替循环调用
@Transactional
public void processItems(List<Item> items) {
    itemProcessor.processBatch(items); // 一个 REQUIRES_NEW 调用处理批量
}

3.2 NESTED 和数据库支持

// 检查数据库是否支持 Savepoint
// MySQL InnoDB: 支持
// MySQL MyISAM: 不支持,NESTED 会退化为 REQUIRED
// Oracle: 支持
// PostgreSQL: 支持

// 检测代码
Connection conn = dataSource.getConnection();
DatabaseMetaData meta = conn.getMetaData();
boolean supportsSavepoints = meta.supportsSavepoints();

// 在不支持 Savepoint 时,Spring 会抛异常或降级
// 可以通过配置强制使用
@Transactional(propagation = Propagation.NESTED,
    rollbackFor = Exception.class)
// 如果数据库不支持 Savepoint,会抛出
// NestedTransactionNotSupportedException

3.3 传播行为和异常处理的组合

// 最常见的坑:REQUIRES_NEW + 异常被吞
@Transactional
public void methodA() {
    try {
        methodB(); // REQUIRES_NEW
    } catch (Exception e) {
        // ❌ 吞掉异常
        // methodB 的事务已经回滚了
        // 但 methodA 认为 B 失败了却继续执行
    }
    doSomethingElse(); // 继续执行,可能导致数据不一致
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    throw new RuntimeException("失败");
    // B 的事务回滚
    // 但 A 吞掉了异常
}

四、工程选型

4.1 传播行为选择指南

场景推荐传播行为原因
普通业务方法REQUIRED数据一致性
日志记录(不影响主业务)REQUIRES_NEW日志需要独立提交
发送消息(事务后发送)REQUIRES_NEW + afterCommit避免消息发出但事务回滚
查询方法SUPPORTS不强制事务,调用者可能有
大数据量处理NOT_SUPPORTED事务影响性能
核心方法(必须事务)MANDATORY强制调用者开启事务
子操作需要独立回滚NESTEDSavepoint 控制回滚范围

4.2 实战配置示例

// 日志服务 - 需要独立提交
@Service
public class AuditLogService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void audit(String action, String details) {
        auditLogRepository.save(new AuditLog(action, details));
    }
}

// 消息发送服务 - 事务后发送
@Service
public class MessageService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendAfterCommit(Message message, Long transactionId) {
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    messageBroker.send(message);
                }
            }
        );
        // 保存消息记录
        messageRepository.save(message);
    }
}

// 查询服务 - 不强制事务
@Service
public class QueryService {
    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public User findById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

五、面试总结

事务传播行为是 Spring 事务中最考察理解的知识点。

P5 候选人能说出 7 种传播行为的名称。 P6 候选人能说清 REQUIRED、REQUIRES_NEW、NESTED 的区别,能解释"挂起"和"Savepoint"的概念。 P7 候选人能说出每种传播行为的适用场景和限制,能解释为什么 NESTED 可能退化,能分析传播行为和异常处理的组合问题。

记住,传播行为不是背名字,而是理解"事务边界在哪里、回滚影响范围是什么"。