事务传播行为是 Spring 事务管理中容易被误解却极为重要的特性。它回答了这样一个问题:当一个带有事务的方法调用另一个带有事务的方法时,事务应该如何流转? 理解传播行为的底层机制,是避免数据不一致性和死锁问题的关键。
6.3.1 底层核心:事务同步与挂起/恢复
Spring 的事务管理并非直接操作数据库连接,而是通过 AbstractPlatformTransactionManager 及其子类(如 DataSourceTransactionManager)协同 TransactionSynchronizationManager 共同完成的。理解传播行为的底层,需要明确两个核心机制:
1. ThreadLocal 绑定当前事务资源
TransactionSynchronizationManager 将当前线程的事务资源(数据库连接、会话)存储在 ThreadLocal 中。当某个方法需要加入当前事务时,它会直接复用该绑定资源;当需要独立事务时,就必须将原有资源“挂起”,并绑定新的资源。
关键的内部结构(简化):
resources:ThreadLocal<Map<Object, Object>>,key 为数据源,value 为当前事务的 ConnectionHolder(持有连接)。synchronizations:ThreadLocal<Set<TransactionSynchronization>>,注册事务同步回调(如 afterCommit、afterCompletion)。currentTransactionName、currentTransactionReadOnly、currentTransactionIsolationLevel:存储当前事务的属性。
2. 事务的挂起与恢复
当传播行为要求创建一个新的独立事务时(如 REQUIRES_NEW),Spring 需要执行以下步骤:
- 挂起当前事务:取出
ThreadLocal中的数据库连接和同步回调,清空这些绑定,然后返回一个SuspendedResourcesHolder对象,其中持有被解绑的连接和回调。 - 创建新事务:从数据源获取一个新的连接,开始新事务,并将新连接绑定到
ThreadLocal上。 - 业务执行:被调用的方法在新事务中运行。
- 恢复原事务:新事务提交/回滚后,使用
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)实现局部回滚。
NESTED 和 REQUIRES_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)。JpaTransactionManager 和 DataSourceTransactionManager 都支持,但 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_NEW 或 NESTED,其余四种极少使用。简单、清晰的事务模型更容易维护,也减少了排查问题的难度。