事务传播 (Transaction Propagation)
本节介绍了 Spring 中事务传播的一些语义。请注意,本节不是对事务传播的正式介绍,而是详细说明了 Spring 中关于事务传播的一些语义。
在 Spring 管理的事务中,请注意物理事务 (Physical Transaction) 和逻辑事务 (Logical Transaction) 之间的区别,以及传播设置如何应用于这种差异。
理解 PROPAGATION_REQUIRED

PROPAGATION_REQUIRED 强制执行物理事务,如果当前没有事务存在,则在本地为当前范围创建一个事务;或者参与为一个更大范围定义的现有“外部”事务。在同一线程内的常见调用堆栈安排中,这是一个很好的默认设置(例如,服务门面委托给多个存储库方法,其中所有底层资源都必须参与服务级事务)。
注意
默认情况下,参与事务会加入外部范围的特性,静默地忽略本地的隔离级别、超时值或只读标志(如果有)。如果你希望在参与具有不同隔离级别的现有事务时拒绝隔离级别声明,请考虑将事务管理器上的 validateExistingTransactions 标志切换为 true。这种非宽松模式还会拒绝只读不匹配(即,尝试参与只读外部范围的内部读写事务)。
当传播设置是 PROPAGATION_REQUIRED 时,会为应用该设置的每个方法创建一个逻辑事务范围。每个这样的逻辑事务范围都可以单独确定“仅回滚” (rollback-only) 状态,外部事务范围在逻辑上独立于内部事务范围。在标准 PROPAGATION_REQUIRED 行为的情况下,所有这些范围都映射到同一个物理事务。因此,在内部事务范围中设置的“仅回滚”标记确实会影响外部事务实际提交的机会。
然而,在内部事务范围设置了“仅回滚”标记的情况下,外部事务并没有决定回滚本身,因此回滚(由内部事务范围静默触发)是意外的。此时会抛出相应的 UnexpectedRollbackException。这是预期的行为,以便事务的调用者永远不会被误导以为执行了提交,而实际上并没有。因此,如果可以通过静默地将事务标记为仅回滚来使内部事务(外部调用者不知道该事务)回滚,而外部调用者仍然调用提交。外部调用者需要接收 UnexpectedRollbackException 以清楚地表明实际上执行了回滚。
理解 PROPAGATION_REQUIRES_NEW

与 PROPAGATION_REQUIRED 相比,PROPAGATION_REQUIRES_NEW 始终为每个受影响的事务范围使用独立的物理事务,永远不参与外部范围的现有事务。在这种安排中,底层资源事务是不同的,因此可以独立提交或回滚,外部事务不受内部事务回滚状态的影响,内部事务的锁在其完成后立即释放。这种独立的内部事务还可以声明自己的隔离级别、超时和只读设置,而不继承外部事务的特性。
注意
附加到外部事务的资源将保持绑定状态,而内部事务将获取自己的资源,例如新的数据库连接。这可能会导致连接池耗尽,如果多个线程具有活动的外部事务并等待获取其内部事务的新连接,而池无法再分发任何此类内部连接,则可能导致死锁。除非你的连接池大小合适(至少超过并发线程数 1),否则不要使用 PROPAGATION_REQUIRES_NEW。
理解 PROPAGATION_NESTED
PROPAGATION_NESTED 使用单个物理事务,该事务具有多个可以回滚到的保存点 (savepoints)。这种部分回滚允许内部事务范围触发其范围的回滚,尽管某些操作已回滚,外部事务仍能继续物理事务。此设置通常映射到 JDBC 保存点,因此它仅适用于 JDBC 资源事务。请参阅 Spring 的 DataSourceTransactionManager。
补充教学
1. 图解物理事务 vs 逻辑事务
理解这个概念是掌握 Spring 事务传播的关键。
- 物理事务 (Physical Transaction):指数据库层面的真实事务(如 JDBC Connection 上的
setAutoCommit(false)...commit())。物理事务的开启和提交通常涉及昂贵的资源操作。 - 逻辑事务 (Logical Transaction):指 Spring 代码层面的
@Transactional方法范围。
在 PROPAGATION_REQUIRED(默认)模式下:
- ServiceA.methodA (外层):开启物理事务,开启逻辑事务A。
- 调用 ServiceB.methodB (内层):不开启新物理事务,直接使用A的物理连接。开启逻辑事务B。
因为它们共享同一个物理连接,所以是一根绳上的蚂蚱。
2. UnexpectedRollbackException 的经典案发现场
这是初学者最容易遇到的异常。场景如下:
@Transactional // 默认 REQUIRED
public void outer() {
try {
innerService.inner(); // 调用内部事务方法
} catch (RuntimeException e) {
// 只有这里捕获了异常,我以为万事大吉,可以继续提交 outer 的事务
logger.error("内部出错了,但我能处理", e);
}
}
@Transactional // 默认 REQUIRED
public void inner() {
throw new RuntimeException("内部挂了");
}结果:outer 方法结束时,Spring 试图提交事务,却抛出 UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only。 原因:
inner抛出异常,Spring 的事务拦截器捕获到,发现它是REQUIRED且加入了现有事务,于是它静默地将物理事务标记为setRollbackOnly()。outer捕获了异常,继续运行直到结束。outer结束时尝试commit(),发现物理事务已经被打上了“必死”的标记,无法提交,只能回滚,并抛出异常通知你“意不意外?惊不惊喜?”。
解决方案:如果希望内部失败不影响外部,内部方法必须使用 REQUIRES_NEW(开启全新物理事务)或 NESTED(使用保存点)。
3. REQUIRES_NEW 的死锁陷阱
REQUIRES_NEW 虽然好用(隔离性强),但因为它需要同时占用两个数据库连接(一个被挂起的外部事务连接,一个正在运行的内部事务连接),如果连接池太小,极易发生资源池死锁。
- 假设连接池只有 10 个连接。
- 并发来了 10 个请求,都执行到了
outer(),占用了 10 个连接。 - 它们同时请求
inner(),需要再申请 10 个新连接。 - 连接池已空,谁也拿不到新连接,谁也不释放旧连接 -> 死锁。
最佳实践:慎用 REQUIRES_NEW,或者确保连接池足够大。
4. NESTED 的局限性
NESTED 看起来很美(使用 Savepoint 实现部分回滚),但它受到底层实现的强限制:
- JPA/Hibernate 不支持:JPA 标准本身不包含 Savepoint,
JpaTransactionManager默认不支持NESTED。 - 仅限 JDBC:主要用于
DataSourceTransactionManager。 因此在现代基于 JPA 的项目中,NESTED很少被使用。