《Spring Boot 高级》7.1 事务同步与事件

拆开 TransactionSynchronizationManager 的线程绑定登记机制与 TransactionSynchronization 的九个回调,讲清 @TransactionalEventListener 四个 phase 各自挂在哪个回调上、为什么「提交后再发消息」必须靠它,并给出 REQUIRES_NEW 与监听器组合时的三个坑。

本节目标:把「事务提交之后才执行」这件事从注解表面下探到 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_COMMITTED0已提交
STATUS_ROLLED_BACK1已回滚
STATUS_UNKNOWN2结果未知(提交阶段本身抛了异常)

注意 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_COMMITbeforeCommit还在事务里,异常可导致回滚
AFTER_COMMITafterCommit已提交,读库能读到新数据
AFTER_ROLLBACKafterCompletion(STATUS_ROLLED_BACK)仅在回滚时执行
AFTER_COMPLETIONafterCompletion(...)提交或回滚都执行

机制上,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 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计