订单提交后的消息为什么有时查不到订单
一个订单服务在 @Transactional 方法里保存订单,然后调用消息组件发送“订单已创建”。压测时正常,线上偶尔出现消费者收到消息后回查订单返回空。把发送动作挪到事务方法返回之后,问题消失,但新的问题出现:如果事务在返回后回滚,消息已经发出去了。
直觉方案是“先提交再发消息”,但 Spring 的 @Transactional 提交发生在方法返回之后,方法体内拿不到提交完成的时刻。TransactionSynchronizationManager 就是为这个时刻准备的:它让业务代码在事务真正提交后得到一次回调。要用对它,必须先看清它绑定了什么、回调按什么顺序跑、回调里抛异常会发生什么。
资源绑定:ThreadLocal 里到底存了什么
TransactionSynchronizationManager 是 Spring 事务基础设施的“中央委托者”,按线程管理资源与事务同步。它的定位是给资源管理代码使用,而不是典型应用代码。
它维护两类线程局部状态。第一类是资源映射:键通常是资源工厂(例如某个 DataSource),值是当前线程正在使用的活动资源(例如对应的 JDBC Connection)。第二类是事务同步列表:当前线程注册的 TransactionSynchronization 回调。
资源绑定有几个明确约束:
- 同一个键同一时刻只能绑定一个资源,必须先解绑才能重新绑定,不会覆盖。
- 资源管理代码应通过
getResource查询线程绑定资源,而不是自己随意绑定;绑定通常是事务管理器的职责。 - 如果事务同步处于激活状态,可以选择“首次使用时惰性绑定”,以支持跨多个资源的事务。
事务同步的激活与关闭由事务管理器负责,通过 initSynchronization() 和 clearSynchronization() 完成。AbstractPlatformTransactionManager 自动支持这一点,因此 DataSourceTransactionManager、JtaTransactionManager 等标准事务管理器都具备该能力。
这解释了一个常见现象:在 @Transactional 方法内调用 getConnection 多次,拿到的是同一个连接。因为连接被绑定在 DataSource 这个键上,同一线程内复用,事务的提交与回滚才有统一的载体。
同步注册:什么时候能注册,注册后存在哪里
注册回调前必须先判断同步是否激活。isSynchronizationActive() 返回当前线程是否处于事务同步状态;如果为 false,说明要么没有当前事务,要么事务管理器不支持事务同步。此时资源管理代码应当立即做资源清理,而不是注册回调。
isActualTransactionActive() 是另一个独立信号:它表示当前是否真的有一个活动事务,与是否注册了同步回调无关。两者不能互相替代。
注册动作本身很简单:registerSynchronization(TransactionSynchronization synchronization) 把回调追加到当前线程的同步列表。getSynchronizations() 返回的是不可修改的快照列表。
一个容易被忽略的边界:如果同步未激活就调用注册,Spring 会直接抛异常,而不是静默忽略。所以示例代码里常见 if (TransactionSynchronizationManager.isSynchronizationActive()) 判断,并在 else 分支里退化为直接执行。
回调触发顺序与异常路径
TransactionSynchronization 接口定义了事务生命周期各阶段的回调。按提交路径的先后,关键方法依次是:
beforeCommit(boolean readOnly):在事务提交前调用,位于beforeCompletion之前。适合把 O/R Mapping 会话刷到数据库。它不保证事务一定提交,之后仍可能决定回滚。beforeCompletion():在提交或回滚前调用,用于资源清理。即使beforeCommit抛了异常,它仍会被调用。afterCommit():事务提交后调用。afterCompletion(int status):提交或回滚后调用,通过STATUS_COMMITTED、STATUS_ROLLED_BACK、STATUS_UNKNOWN判断结果。
异常语义是这里最关键的分水岭:
beforeCommit抛出的运行时异常会传播给提交调用方,并导致事务回滚。官方文档特别提醒:不要在这里抛TransactionException的子类。beforeCompletion抛出的运行时异常会被记录日志,但不会传播。afterCommit和afterCompletion在事务已经提交之后执行,此时再抛异常无法撤销提交。
顺序控制通过 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 并承担消息侧幂等成本。没有一种方案能同时满足强一致与低耦合,边界就在这里。