人人都会AI编程

6.3 事务传播行为底层逻辑与七种传播机制

更新时间:2026-07-11

事务传播行为是 Spring 事务管理中容易被误解却极为重要的特性。它回答了这样一个问题:当一个带有事务的方法调用另一个带有事务的方法时,事务应该如何流转? 理解传播行为的底层机制,是避免数据不一致性和死锁问题的关键。

6.3.1 底层核心:事务同步与挂起/恢复

Spring 的事务管理并非直接操作数据库连接,而是通过 AbstractPlatformTransactionManager 及其子类(如 DataSourceTransactionManager)协同 TransactionSynchronizationManager 共同完成的。理解传播行为的底层,需要明确两个核心机制:

1. ThreadLocal 绑定当前事务资源

TransactionSynchronizationManager 将当前线程的事务资源(数据库连接、会话)存储在 ThreadLocal 中。当某个方法需要加入当前事务时,它会直接复用该绑定资源;当需要独立事务时,就必须将原有资源“挂起”,并绑定新的资源。

关键的内部结构(简化):

  • resourcesThreadLocal<Map<Object, Object>>,key 为数据源,value 为当前事务的 ConnectionHolder(持有连接)。
  • synchronizationsThreadLocal<Set<TransactionSynchronization>>,注册事务同步回调(如 afterCommit、afterCompletion)。
  • currentTransactionNamecurrentTransactionReadOnlycurrentTransactionIsolationLevel:存储当前事务的属性。

2. 事务的挂起与恢复

当传播行为要求创建一个新的独立事务时(如 REQUIRES_NEW),Spring 需要执行以下步骤:

  1. 挂起当前事务:取出 ThreadLocal 中的数据库连接和同步回调,清空这些绑定,然后返回一个 SuspendedResourcesHolder 对象,其中持有被解绑的连接和回调。
  2. 创建新事务:从数据源获取一个新的连接,开始新事务,并将新连接绑定到 ThreadLocal 上。
  3. 业务执行:被调用的方法在新事务中运行。
  4. 恢复原事务:新事务提交/回滚后,使用 SuspendedResourcesHolder 重新将原来的连接和同步信息绑定回 ThreadLocal

这个挂起/恢复过程在物理层面意味着需要获取独立的数据库连接。因此,如果连接池大小不足,嵌套的 REQUIRES_NEW 可能导致连接饥饿或死锁,这是实际生产中多次出现过的坑。

6.3.2 七种传播机制的实用解析

Spring 定义了七种传播行为,定义在 Propagation 枚举中。下面逐一讲解,并辅以真实场景说明。

PROPAGATION_REQUIRED(默认值)

语义:如果当前存在事务,则加入该事务;如果没有,则创建一个新事务。

这是最常用的传播行为,也是 @Transactional 的默认设置。它确保了多个操作在一个事务上下文内执行,任意一处失败都会导致整体回滚。

@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
    orderDao.insert(order);
    inventoryService.reduceStock(order); // 该方法也标注了 REQUIRED
}

这里 createOrder 启动了一个事务,reduceStock 加入该事务,它们共享同一个数据库连接,任何一步失败都会回滚整个订单创建操作。

PROPAGATION_REQUIRES_NEW

语义:无论当前是否存在事务,都创建一个新的事务。如果有当前事务,则将其挂起。

典型场景:操作日志记录。记录日志应该独立于业务事务,即使业务回滚,日志也必须提交。

@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
    accountDao.debit(from, amount);
    accountDao.credit(to, amount);
    auditService.logTransfer(from, to, amount); // 日志应单独提交
}

@Service
public class AuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logTransfer(...) {
        // 独立事务,即使外部回滚,该日志也会持久化
    }
}

需要注意:REQUIRES_NEW 创建的是完全独立的事务,内层事务的提交与外层事务的回滚互不干扰。如果内层方法抛出了异常并导致内层回滚,但异常被外层捕获且未传播,则外层可以正常提交。

PROPAGATION_NESTED

语义:如果当前存在事务,则在嵌套事务内执行;如果没有,则创建一个新事务(行为同 REQUIRED)。嵌套事务与父事务共享连接,但可以设置保存点(savepoint)实现局部回滚。

NESTEDREQUIRES_NEW 的最大区别在于:NESTED 不挂起外层事务,而是利用数据库的 savepoint(保存点)机制,形成逻辑上的子事务。子事务回滚只影响自身,父事务可以选择提交或回滚;但如果父事务回滚,子事务也会一同回滚。

@Transactional
public void processBatch(List<Record> records) {
    for (Record record : records) {
        try {
            dataService.processRecord(record); // NESTED
        } catch (Exception e) {
            // 单条记录失败不影响整体
        }
    }
}

@Service
public class DataService {
    @Transactional(propagation = Propagation.NESTED)
    public void processRecord(Record record) {
        // 可能抛出异常,但只回滚到该方法开始时的保存点
    }
}

前提:使用的数据库平台和驱动必须支持 savepoint(如 MySQL InnoDB、PostgreSQL、Oracle)。JpaTransactionManagerDataSourceTransactionManager 都支持,但 JtaTransactionManager 在分布式事务中不支持 NESTED

PROPAGATION_SUPPORTS

语义:如果当前存在事务,则加入事务;如果不存在,则以非事务方式执行。

用于“事务可选”的方法,比如只读查询。它们单独执行时不需要事务开销,但在一个已有事务的链中被调用时,又能参与到该事务中,保证读取的一致性。

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public Order getOrder(Long id) {
    return orderDao.findById(id);
}

PROPAGATION_NOT_SUPPORTED

语义:总是以非事务方式执行。如果有当前事务,则将其挂起,直到方法执行完毕再恢复。

很少使用,典型场景是不希望关键操作受到当前事务的影响,例如某些必须独立执行的 DDL 语句,或者调用存储过程时不想参与当前事务。

PROPAGATION_MANDATORY

语义:必须在当前存在的事务中运行,否则抛出 IllegalTransactionStateException 异常。

用于强制要求调用者必须提供事务上下文的场合,保证方法不会在无事务的环境中误用,通常用于关键的业务规则校验。

@Transactional(propagation = Propagation.MANDATORY)
public void validateOrderConsistency(Order order) {
    // 只有被其他事务方法调用时才有效,单独调用立即报错
}

PROPAGATION_NEVER

语义:必须在非事务环境中执行,如果当前存在事务,则抛出异常。

这个传播行为极为罕见,常用于强制排除事务干扰的场景,例如某些对性能要求极高、希望完全绕过事务管理的纯查询操作,且明确不希望被事务包裹。

6.3.3 常见陷阱与最佳实践

1. REQUIRES_NEW 导致连接耗尽

由于每个 REQUIRES_NEW 方法都需要从数据源获取新的数据库连接,如果多个外层事务并发调用多个 REQUIRES_NEW 方法,连接数会成倍增长。合理设置连接池大小,并避免在循环中频繁调用 REQUIRES_NEW 方法是必要的。

2. 自调用导致 AOP 失效

传播行为依赖于 Spring AOP 代理,如果在同一个类中通过 this 调用事务方法,代理不会被触发,传播行为完全失效。解决方案是将内部事务方法抽取到另一个 Bean 中,或者通过 AopContext.currentProxy() 调用。

3. 异常捕获与回滚范围

REQUIRED 嵌套中,如果内层方法抛出被捕获的异常,外层仍可提交。需要谨慎处理异常是否会导致不必要的提交。

4. 传播行为组合的复杂性

在生产代码中,不建议过度依赖复杂的传播行为嵌套。大多数场景使用 REQUIRED 即可,特定场景(日志、批处理)使用 REQUIRES_NEWNESTED,其余四种极少使用。简单、清晰的事务模型更容易维护,也减少了排查问题的难度。