本节目标:把「事务提交之后才执行」这件事从注解表面下探到
TransactionSynchronizationManager的登记机制,讲清TransactionSynchronization回调的真实触发时机,以及@TransactionalEventListener四个 phase 分别对应哪一个回调。
适用版本:Spring Boot 4.1.x(Java 21)
实战卷解决的是「怎么配」——@Transactional 的传播行为怎么选、只读事务怎么开。本节解决「为什么这样配」:当一个方法体里既改数据库又要发消息时,框架凭什么能保证「消息不会在事务回滚后发出去」。答案不在 @Transactional 上,而在 spring-tx 的一层同步回调机制里。
本节围绕一个订单场景演进:OrderService.placeOrder() 在事务里写订单并发布 OrderPlacedEvent,OrderNotificationListener 负责通知下游。7.2 与 7.3 会继续用这套对象讲异步执行与线程池。
7.1.1 TransactionSynchronizationManager 是线程绑定的登记处
org.springframework.transaction.support.TransactionSynchronizationManager 是一个纯静态工具类,它把两样东西绑在当前线程上(方法名均已从本机 spring-tx-7.0.9.jar 核实):
| 静态方法 | 作用 |
|---|---|
bindResource(Object key, Object value) | 把资源绑到当前线程,例如把 DataSource 绑成 ConnectionHolder |
getResource(Object key) | 取回当前线程绑定的资源 |
unbindResource(Object key) | 解绑 |
initSynchronization() | 开启同步回调登记(内部把 synchronizations 置为非 null) |
isSynchronizationActive() | 判断当前线程是否处于「可登记回调」状态 |
registerSynchronization(TransactionSynchronization) | 登记一个回调对象 |
getSynchronizations() | 取回本线程已登记的回调列表 |
clearSynchronization() | 清空回调列表 |
这些状态都存在 ThreadLocal 里,所以「事务同步」天然是单线程概念:事务在哪条线程上开、回调就在哪条线程上被登记和触发。这也解释了为什么跨线程传播事务上下文不是自动的——换线程就等于换了一整套登记处。
关键点:initSynchronization() 不是 @Transactional 直接调的,而是 AbstractPlatformTransactionManager.prepareSynchronization(...) 在事务开始时调的。也就是说,只有真正开启了事务,登记处才打开;一个没有事务的方法里调 registerSynchronization 会直接抛 IllegalStateException。
7.1.2 TransactionSynchronization 的九个回调与顺序
TransactionSynchronization 是一个接口,除 getOrder() 外全部是 default 方法(本机 javap 核实):
public interface TransactionSynchronization extends Ordered, Flushable {
default int getOrder();
default void suspend();
default void resume();
default void flush();
default void savepoint(Object savepoint);
default void savepointRollback(Object savepoint);
default void beforeCommit(boolean readOnly);
default void beforeCompletion();
default void afterCommit();
default void afterCompletion(int status);
}
afterCompletion(int) 的 status 取值是三个常量(javap 核实):
| 常量 | 值 | 含义 |
|---|---|---|
STATUS_COMMITTED | 0 | 已提交 |
STATUS_ROLLED_BACK | 1 | 已回滚 |
STATUS_UNKNOWN | 2 | 结果未知(提交阶段本身抛了异常) |
注意 beforeCommit 和 beforeCompletion 是两个不同的时机:beforeCommit 在提交前、还能通过抛异常阻止提交;beforeCompletion 在 beforeCommit 之后、无论最终提交还是回滚都会执行一次。afterCompletion 则是收尾,无论成败都会走。
回调是有序的:TransactionSynchronization 继承 Ordered,getOrder() 默认返回最低优先级;TransactionSynchronizationUtils 在触发前会按 getOrder() 排序。这保证「连接释放」这类基础设施回调能稳定地排在业务回调之后——DataSourceUtils 里有一个 CONNECTION_SYNCHRONIZATION_ORDER 常量(本机 javap 核实其存在)专门用来钉住这个次序。
7.1.3 AbstractPlatformTransactionManager 何时触发回调
AbstractPlatformTransactionManager 把「触发回调」和「真正提交/回滚」拆成了两组方法(javap 核实):
commit(TransactionStatus)(public final)内部走processCommit(...)rollback(TransactionStatus)(public final)内部走processRollback(...)- 触发回调的四个私有/受保护方法:
triggerBeforeCommit、triggerBeforeCompletion、triggerAfterCommit、triggerAfterCompletion
一次成功提交的调用序列大致是:
commit(status)
└─ processCommit(status)
├─ triggerBeforeCommit(status) → 所有 beforeCommit(readOnly)
├─ triggerBeforeCompletion(status) → 所有 beforeCompletion()
├─ doCommit(status) ← 真正写库
├─ triggerAfterCommit(status) → 所有 afterCommit()
└─ triggerAfterCompletion(status, STATUS_COMMITTED)
一次回滚则是:
rollback(status)
└─ processRollback(status, ...)
├─ triggerBeforeCompletion(status) → 所有 beforeCompletion()
├─ doRollback(status) ← 真正回滚
└─ triggerAfterCompletion(status, STATUS_ROLLED_BACK)
对照表能直接读出「什么时候写什么代码」:
| 我想做的事 | 挂哪个回调 | 能否阻止提交 |
|---|---|---|
| 提交前校验、必要时中止 | beforeCommit | 能(抛异常) |
| 无论成败都清理资源 | beforeCompletion | 不能 |
| 提交成功后发消息、刷缓存 | afterCommit | 不能 |
| 区分提交/回滚做补偿 | afterCompletion(status) | 不能 |
顺带说明资源映射是怎么用的。bindResource 最常见的调用者是 DataSourceUtils 与 DataSourceTransactionManager:它们把 DataSource 当 key、ConnectionHolder 当 value 绑到当前线程,语义大致是:
// 简化后的语义,非源码逐行
ConnectionHolder holder =
(ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (holder != null) {
return holder.getConnection(); // 同一事务内复用同一条物理连接
}
这正是「一个事务里多次 getConnection() 拿到同一条连接」的实现原理,也是 @Transactional 能保证多条 SQL 同连接、同事务的底层支撑。调试连接泄漏时,TransactionSynchronizationManager.getResourceMap() 能直接看到当前线程绑了哪些资源。
7.1.4 @TransactionalEventListener 的四个 phase
@TransactionalEventListener 的 phase() 默认值是 AFTER_COMMIT(本机 javap -v 读到 AnnotationDefault 确认为 TransactionPhase.AFTER_COMMIT),fallbackExecution() 默认 false。四个 phase 与底层回调一一对应:
TransactionPhase | 挂在哪个同步回调 | 语义 |
|---|---|---|
BEFORE_COMMIT | beforeCommit | 还在事务里,异常可导致回滚 |
AFTER_COMMIT | afterCommit | 已提交,读库能读到新数据 |
AFTER_ROLLBACK | afterCompletion(STATUS_ROLLED_BACK) | 仅在回滚时执行 |
AFTER_COMPLETION | afterCompletion(...) | 提交或回滚都执行 |
机制上,TransactionalEventListenerFactory 把带该注解的方法包装成 TransactionalApplicationListenerMethodAdapter;onApplicationEvent 被调用时,它不会立刻执行业务方法,而是通过 TransactionSynchronizationManager.registerSynchronization(...) 登记一个内部 TransactionSynchronization,把真正的调用推迟到对应回调里。也就是说,事件是在事务内发布的,执行却发生在事务边界之后。
7.1.5 为什么「提交后再发消息」必须靠它
假设不用这套机制,直接在事务方法里 applicationEventPublisher.publishEvent(new OrderPlacedEvent(id)):
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
publisher.publishEvent(new OrderPlacedEvent(order.getId())); // 同步、立即执行
}
publishEvent 默认是同步的:监听器在当前线程、当前事务里被立刻调用。此时事务还没提交,监听器若去查订单(哪怕另起一个连接)可能查不到;更要命的是,若监听器里发了 MQ 消息而后续事务回滚,消息已经出去了,数据库却没有这笔订单——下游拿到一个不存在的订单号。
@TransactionalEventListener(phase = AFTER_COMMIT) 解决的正是这个:把「发消息」推迟到 afterCommit 回调。此时事务已提交,数据可见,消息与数据达成一致。代价是放弃了事务的原子性保证——afterCommit 里发消息失败不会回滚业务事务,这部分要靠重试或本地消息表补齐。
7.1.6 REQUIRES_NEW 与监听器组合的三个坑
坑一:内层 REQUIRES_NEW 会让 AFTER_COMMIT 提前触发。 若 placeOrder 是 REQUIRED,内部又调了一个 REQUIRES_NEW 的方法并在其中发布事件,那么 afterCommit 挂的是内层事务的提交。内层一提交,监听器就跑,此时外层事务可能还没提交甚至可能回滚——「提交后执行」的直觉被打破。
坑二:监听器自身带 @Transactional(REQUIRES_NEW)。 如果监听器是 BEFORE_COMMIT 又想写库,它会在同一事务里执行,异常会连带业务回滚;若显式开 REQUIRES_NEW,就变成独立事务,外层回滚也不影响它,反而可能留下「业务没成功、通知却写下了」的脏数据。要保证一致,监听器要么只读、要么在 AFTER_COMMIT 里执行。
坑三:没有活动事务时监听器不执行。 因为登记处只在事务中打开。若 publishEvent 发生在事务之外,AFTER_COMMIT 监听器不会被调用。需要「无论有没有事务都执行」时,加 fallbackExecution = true:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
public void onOrderPlaced(OrderPlacedEvent event) {
notificationService.notify(event.orderId());
}
7.1.7 手动登记一个 TransactionSynchronization
不想引入事件机制、想直接挂钩回调时,可以手动登记。下面的 AuditRecorder 在同一个事务里既写业务、又在提交后写审计:
@Service
public class AuditRecorder {
private final AuditSink auditSink;
public AuditRecorder(AuditSink auditSink) {
this.auditSink = auditSink;
}
@Transactional
public void record(Long orderId) {
// ...业务写库
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
auditSink.write("order-committed:" + orderId);
}
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
auditSink.write("order-rolled-back:" + orderId);
}
}
});
}
}
匿名内部类里引用的 orderId 必须是 effectively final。这个写法与一个手写的 @TransactionalEventListener 等价,区别在于它能在同一个回调对象里同时处理「提交」与「回滚」两个分支,无需再发一次事件。若只是发消息,用注解更省事;若要精细控制多个回调的顺序,手动登记 getOrder() 更直接。
7.1.8 两种发布方式的对比
| 维度 | 直接 publishEvent | @TransactionalEventListener |
|---|---|---|
| 执行时机 | 立即、同步、事务内 | 事务边界之后(默认提交后) |
| 事务回滚的影响 | 监听器已经执行过 | 提交前不会执行 |
| 能否读到自己写的数据 | 可能读不到(尚未提交) | AFTER_COMMIT 里能读到 |
| 失败是否回滚业务事务 | 取决于是否同一事务 | 不影响业务事务 |
| 无事务时的行为 | 照常执行 | 默认不执行,除非 fallbackExecution = true |
7.1.9 怎么验证这套机制
断点。 在 AbstractPlatformTransactionManager.triggerAfterCommit 与 TransactionSynchronizationUtils.triggerAfterCommit 上各下一个断点,观察 getSynchronizations() 列表里到底登记了几个回调——一个 @TransactionalEventListener 就是一个,加上连接释放的 ConnectionSynchronization 通常会看到不止一个。
日志。 给监听器加一行 log.info("phase=AFTER_COMMIT, txActive={}", TransactionSynchronizationManager.isActualTransactionActive()),在 AFTER_COMMIT 里会打印 false(事务已结束),在 BEFORE_COMMIT 里会打印 true。这一条就能把四个 phase 的差别验出来。
主动制造回滚。 在 placeOrder 里保存订单后抛 RuntimeException,会看到 AFTER_COMMIT 监听器完全不执行、AFTER_ROLLBACK 监听器执行——证明事件发布在事务内、执行在边界外。
7.1.10 排障清单
| 现象 | 根因 | 处理 |
|---|---|---|
AFTER_COMMIT 监听器完全不执行 | 发布时没有活动事务 | 加 fallbackExecution = true,或把发布放进事务 |
| 监听器里查到旧数据 | 监听器在 BEFORE_COMMIT 执行 | 改成 AFTER_COMMIT |
| 消息发出但数据回滚 | 用了直接 publishEvent 且同事务 | 改用 AFTER_COMMIT + 补偿 |
| 监听器抛异常导致业务回滚 | 监听器是 BEFORE_COMMIT 且异常未捕获 | 移出事务,或改用 AFTER_COMPLETION |
registerSynchronization 抛 IllegalStateException | 当前线程没有活动事务 | 确认调用点确实在 @Transactional 内 |
内层 REQUIRES_NEW 一提交监听器就跑了 | 回调挂到了内层事务边界 | 调整传播行为,或改为外层统一发布 |
补充一个容易忽略的机制:REQUIRES_NEW 会挂起外层事务,TransactionSynchronization 的 suspend() / resume() 回调就是为此准备的——挂起时当前线程的同步回调列表被换出,恢复时再换回。所以嵌套 REQUIRES_NEW 期间,外层登记的回调既不会被内层触发,也不会在内层提交时被清理。
小结
- 事务同步是线程绑定的:
TransactionSynchronizationManager用ThreadLocal存资源映射与回调列表,只有事务开启时登记处才可用。 TransactionSynchronization的beforeCommit/beforeCompletion/afterCommit/afterCompletion由AbstractPlatformTransactionManager的trigger*方法在processCommit/processRollback中按固定顺序触发。@TransactionalEventListener的四个 phase 就是这四个回调的别名;默认AFTER_COMMIT,fallbackExecution默认false。- 「提交后再发消息」靠的是把执行推迟到
afterCommit;代价是失去原子性,需自行补偿。REQUIRES_NEW会让「提交」的语义提前到内层边界,务必分清。
下一节换到执行侧:当监听器要真正「发消息」这类阻塞动作时,用虚拟线程承接会怎样改变 Spring 的线程模型。
阅读导航:上一节:6.3 连接获取与延迟加载 · 下一节:7.2 虚拟线程下的 Spring 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。