Java 技术
#Spring#事务管理#ThreadLocal#事务同步

Spring 事务同步管理器:资源绑定与事务完成回调的执行顺序

订单事务提交后发送消息或清缓存时,为什么回调有时读不到数据、有时又让事务回滚?本文拆解 TransactionSynchronizationManager 的 ThreadLocal 资源绑定、同步注册与触发顺序,说明 beforeCommit 抛异常与 afterCommit 抛异常的差别,并给出可观测与降级思路。

订单提交后的消息为什么有时查不到订单

一个订单服务在 @Transactional 方法里保存订单,然后调用消息组件发送“订单已创建”。压测时正常,线上偶尔出现消费者收到消息后回查订单返回空。把发送动作挪到事务方法返回之后,问题消失,但新的问题出现:如果事务在返回后回滚,消息已经发出去了。

直觉方案是“先提交再发消息”,但 Spring 的 @Transactional 提交发生在方法返回之后,方法体内拿不到提交完成的时刻。TransactionSynchronizationManager 就是为这个时刻准备的:它让业务代码在事务真正提交后得到一次回调。要用对它,必须先看清它绑定了什么、回调按什么顺序跑、回调里抛异常会发生什么。

资源绑定:ThreadLocal 里到底存了什么

TransactionSynchronizationManager 是 Spring 事务基础设施的“中央委托者”,按线程管理资源与事务同步。它的定位是给资源管理代码使用,而不是典型应用代码。

它维护两类线程局部状态。第一类是资源映射:键通常是资源工厂(例如某个 DataSource),值是当前线程正在使用的活动资源(例如对应的 JDBC Connection)。第二类是事务同步列表:当前线程注册的 TransactionSynchronization 回调。

资源绑定有几个明确约束:

  • 同一个键同一时刻只能绑定一个资源,必须先解绑才能重新绑定,不会覆盖。
  • 资源管理代码应通过 getResource 查询线程绑定资源,而不是自己随意绑定;绑定通常是事务管理器的职责。
  • 如果事务同步处于激活状态,可以选择“首次使用时惰性绑定”,以支持跨多个资源的事务。

事务同步的激活与关闭由事务管理器负责,通过 initSynchronization()clearSynchronization() 完成。AbstractPlatformTransactionManager 自动支持这一点,因此 DataSourceTransactionManagerJtaTransactionManager 等标准事务管理器都具备该能力。

这解释了一个常见现象:在 @Transactional 方法内调用 getConnection 多次,拿到的是同一个连接。因为连接被绑定在 DataSource 这个键上,同一线程内复用,事务的提交与回滚才有统一的载体。

同步注册:什么时候能注册,注册后存在哪里

注册回调前必须先判断同步是否激活。isSynchronizationActive() 返回当前线程是否处于事务同步状态;如果为 false,说明要么没有当前事务,要么事务管理器不支持事务同步。此时资源管理代码应当立即做资源清理,而不是注册回调。

isActualTransactionActive() 是另一个独立信号:它表示当前是否真的有一个活动事务,与是否注册了同步回调无关。两者不能互相替代。

注册动作本身很简单:registerSynchronization(TransactionSynchronization synchronization) 把回调追加到当前线程的同步列表。getSynchronizations() 返回的是不可修改的快照列表。

一个容易被忽略的边界:如果同步未激活就调用注册,Spring 会直接抛异常,而不是静默忽略。所以示例代码里常见 if (TransactionSynchronizationManager.isSynchronizationActive()) 判断,并在 else 分支里退化为直接执行。

回调触发顺序与异常路径

TransactionSynchronization 接口定义了事务生命周期各阶段的回调。按提交路径的先后,关键方法依次是:

  1. beforeCommit(boolean readOnly):在事务提交前调用,位于 beforeCompletion 之前。适合把 O/R Mapping 会话刷到数据库。它不保证事务一定提交,之后仍可能决定回滚。
  2. beforeCompletion():在提交或回滚前调用,用于资源清理。即使 beforeCommit 抛了异常,它仍会被调用。
  3. afterCommit():事务提交后调用。
  4. afterCompletion(int status):提交或回滚后调用,通过 STATUS_COMMITTEDSTATUS_ROLLED_BACKSTATUS_UNKNOWN 判断结果。

异常语义是这里最关键的分水岭:

  • beforeCommit 抛出的运行时异常会传播给提交调用方,并导致事务回滚。官方文档特别提醒:不要在这里抛 TransactionException 的子类。
  • beforeCompletion 抛出的运行时异常会被记录日志,但不会传播。
  • afterCommitafterCompletion 在事务已经提交之后执行,此时再抛异常无法撤销提交。

顺序控制通过 Ordered 接口完成。TransactionSynchronization 继承自 Ordered,默认顺序是 Ordered.LOWEST_PRECEDENCE,表示较晚执行;返回值越小越早执行。没有实现 Ordered 的同步会被追加到链尾。Spring 自身的系统级同步使用特定顺序值,以便与业务回调做细粒度交互。

flowchart TD
    A[业务方法保存订单] --> B{同步是否激活}
    B -- 否 --> C[直接发送消息或清缓存]
    B -- 是 --> D[注册事务同步回调]
    D --> E[业务方法返回]
    E --> F[beforeCommit 刷会话]
    F --> G[beforeCompletion 清理资源]
    G --> H{提交是否成功}
    H -- 成功 --> I[afterCommit 发消息清缓存]
    H -- 失败 --> J[afterCompletion 回滚状态]
    I --> K[afterCompletion 提交状态]

这张图对应订单场景的完整路径:注册发生在业务方法内,真正触发发生在方法返回之后,由事务管理器驱动。

与 @TransactionalEventListener 的关系

@TransactionalEventListener 是事务同步机制在事件模型上的封装。它把事件监听器绑定到事务阶段,默认阶段是 AFTER_COMMIT,也就是内部注册一个事务同步,在 afterCommit 时投递事件。

两者不是替代关系,而是层次关系:

维度TransactionSynchronizationManager@TransactionalEventListener
抽象层次底层 API,直接操作同步列表事件模型封装,声明式
注册时机手动调用 registerSynchronization发布事件时自动注册
顺序控制实现 Ordered 或 getOrder依赖监听器顺序与阶段配置
异常影响beforeCommit 异常可回滚默认阶段在提交后,异常不回滚
适用场景需要精确控制阶段与顺序解耦的业务事件通知
学习成本需理解资源绑定与生命周期只需理解事件与阶段

如果订单服务只是“提交后通知下游”,@TransactionalEventListener 更简洁。如果需要精确控制“先清缓存再发消息”,或者需要同时处理 beforeCommit 的会话刷新,直接用 TransactionSynchronizationManager 更可控。

生产场景:订单提交后发消息与清缓存

回到贯穿场景。订单服务在事务内保存订单,提交后需要做两件事:清理订单缓存,发送订单创建消息。两者顺序有讲究——如果先发消息,消费者可能立刻回查缓存,读到旧值。

一种实现是在事务方法内注册一个同步回调,在 afterCommit 中先清缓存再发消息。这里有几个工程判断:

  • 回调只在当前线程有效。如果在回调里 @Async 切到新线程,新线程没有事务上下文,afterCommit 的语义不会自动传递。异步操作需要在回调内显式触发,或通过消息队列解耦。
  • 回调中不要做耗时操作。afterCommit 执行时事务已提交,但调用方线程仍被占用,长耗时操作会拉长响应时间。
  • 发消息失败无法回滚订单。这是 afterCommit 的固有限制:事务已经提交,回调里的异常只能自行处理。工程上通常配合本地消息表或重试队列,把“提交”和“通知”的一致性交给补偿机制。

如果业务要求“消息发不出去就回滚订单”,那就不该放在 afterCommit,而应放在 beforeCommit,让异常传播并触发回滚。代价是消息系统必须支持事务或幂等,否则会出现消息已发但事务回滚的不一致。

可观测信号与失败模式

排查事务同步相关问题,可以观察这些信号:

  • 日志中 beforeCommit 抛出的异常会伴随事务回滚,堆栈指向提交调用方。
  • beforeCompletion 的异常只出现在日志里,事务结果不受影响,容易被误判为“没事”。
  • afterCommit 的异常不会回滚,但会导致后续清理或通知缺失,表现为缓存与数据库不一致。
  • 同步未激活时注册回调会直接抛异常,通常说明事务边界被绕过,例如自调用导致代理失效。

常见失败模式包括:在 afterCommit 里访问已关闭的资源;在回调里再次开启事务导致嵌套;多个回调顺序不符合预期导致缓存与消息不一致;异步回调丢失事务上下文。

适用边界与替代方案

TransactionSynchronizationManager 的收益是精确控制事务生命周期,成本是必须理解线程绑定、顺序和异常语义。它不适合跨线程场景,也不适合分布式事务的最终一致性——那需要消息队列或事务消息。

@TransactionalEventListener 相比,前者更底层、更灵活,后者更声明式、更易维护。与编程式事务相比,同步回调把“提交后动作”从业务逻辑里分离出来,但代价是回调中的异常处理需要自己兜底。

选择哪种方案,取决于一致性要求:能接受提交后通知失败并补偿,用 afterCommit;要求通知失败即回滚,用 beforeCommit 并承担消息侧幂等成本。没有一种方案能同时满足强一致与低耦合,边界就在这里。

资料来源

  1. TransactionSynchronizationManager (Spring API)
  2. spring事务 之 TransactionSynchronizationManager - 蓝迷梦 - 博客园
  3. Spring优雅管理事务回调-腾讯云开发者社区-腾讯云
  4. TransactionSynchronization (Spring Framework 7.0.8 API)