Spring 事务何时真正提交:方法返回、回调与外部副作用
SQL 执行完成、事务方法执行到末尾,都不能单独证明事务已提交。 默认代理模式中,事务拦截器在目标方法返回后处理提交;若方法加入外层事务,还要等外层结束。缓存删除、消息通知等副作用应以成功提交为前提,需要失败恢复时再配合同库持久化待办。
适用版本与边界
本文依据 Spring Framework 6.2 文档,面向 JDK 17/21、代理模式、PlatformTransactionManager 管理的 JDBC 本地事务。假定业务表与 outbox 使用同一数据源和事务管理器。JTA、多数据源及响应式事务需要另外确认边界;不能把线程绑定事务的推理直接套到 Reactor 上下文。
1. 为什么“SQL 后面发通知”仍然太早?
设订单事务依次执行:更新订单 → 发送“已确认”通知 → 返回方法 → 数据库提交失败。数据库没有保留确认结果,通知却已经发出;普通数据库回滚无法撤回外部消息。
另一种时序是外层 A 调用 REQUIRED 方法 B。B 返回后 A 继续执行并失败回滚,B 的修改也随之撤销,因为它们共享物理事务。根据 Spring 传播机制文档,逻辑方法边界不等于独立物理提交边界。
| 观察位置 | 能确认什么 | 仍不能确认什么 |
|---|---|---|
| SQL 返回或 ORM flush 完成 | 相关语句已发送执行 | 事务最终一定提交 |
| 目标方法正常返回 | 业务方法主体未向外抛错 | 拦截器提交成功、外层不会回滚 |
| beforeCommit 回调 | 已进入提交前阶段 | 后续操作一定成功 |
| afterCommit 回调 | 当前物理事务已经成功提交 | 外部动作完成、其他副本立即可见 |
TransactionSynchronization API明确指出 beforeCommit 不保证最终提交。日志应区分“方法执行完”和“提交完成”,否则容易把提交失败误判为通知丢失。
2. 提交后事件能保证消息不丢吗?
@TransactionalEventListener 默认绑定 AFTER_COMMIT;无事务发布时默认不调用监听器。启用 fallbackExecution 只是允许无事务时执行,不能创造提交保证。官方事务事件说明
下面是未实际执行的说明性代码片段:两个方法分别放在受 Spring 管理的业务 Bean 和监听 Bean 中,省略注入、导入与仓储实现;调用业务方法须经过事务代理。
record OrderChanged(long orderId) {}
// 业务 Bean:仓储均参加当前同库事务
@Transactional
public void confirm(long id) {
orders.confirm(id);
outbox.insertPending(id);
publisher.publishEvent(new OrderChanged(id));
}
// 监听 Bean:只唤醒后台扫描器,不直接代表投递完成
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onCommitted(OrderChanged event) {
dispatcher.wakeup();
}若提交后、监听执行前进程退出,内存事件会丢失。这里的可靠性来自事务内写入的 outbox:后台扫描器即使没被唤醒,也要周期扫描已提交待办。它负责发送、失败重试和确认完成;发送成功后未确认便退出会引发重复投递,接收方仍需按事件 ID 或业务键幂等。事件监听只缩短等待,不替代持久化。
3. 提交后再写数据库,为什么还要新事务?
“连接仍能访问”不代表还会再提交。提交后资源可能尚未清理,数据访问可能继续绑定原资源,但原事务已经结束。TransactionalEventListener API专门警告此时的修改不会随原事务再次提交。
确需在回调里写审计记录,应通过另一个 Bean 的代理调用 REQUIRES_NEW,建立独立事务并处理其失败。它会占用额外连接:外层资源尚未释放时,新事务还要申请自己的连接;若并发线程各持有一个外层连接并都等待内层连接,连接池耗尽就可能形成相互等待。应结合并发量与嵌套层数评估余量,见 REQUIRES_NEW 官方说明。也不能用它提前保存本应随订单回滚的 outbox,否则会留下没有业务提交的待办。
直接注册的 TransactionSynchronization.afterCommit 若抛出运行时异常,可传回提交调用方,但已经提交的业务不会因此撤销。调用方不能看到异常就断言“操作没发生”并盲目重试,应先靠业务幂等键查询状态。这里需要区分两个名称相近的入口:
| 入口(本文的同步执行场景) | 执行阶段 | 运行时异常如何处理 |
|---|---|---|
直接注册 TransactionSynchronization.afterCommit() | 物理提交成功后 | 传播给提交调用方,但不能撤销提交 |
@TransactionalEventListener(AFTER_COMMIT) | afterCompletion 的成功提交分支 | 由事务同步调用层记录,不传播给提交调用方,也不能撤销提交 |
TransactionPhase API明确说明,事件的 AFTER_COMMIT 属于 AFTER_COMPLETION,并非调用同名的 afterCommit();同步回调 API则规定了异常记录行为。因此,示例中的 wakeup() 即使失败,调用方也可能正常返回,后台周期扫描仍不可省略;若改用异步监听,还需按执行器的异常处理机制另行观察失败。
4. 代理与异常会怎样改变边界?
默认代理模式下,this.confirm() 不经过代理,方法上的事务声明不会新生效;若调用方已有事务,代码仍可能运行在已有事务中。异步任务也不会自动继承调用线程的本地事务,必须明确自己的事务入口。注解事务文档
未自定义回滚规则时,默认对 RuntimeException 和 Error 回滚,对受检异常不自动回滚,应核对方法与全局配置。内层 REQUIRED 已标记 rollback-only 后,外层即使捕获异常,也可能在提交时收到 UnexpectedRollbackException;捕获异常不等于恢复可提交状态。
5. 如何验证自己找对了提交点?
以下是建议的检查步骤,本文未执行集成测试:
- 确认入口经过哪个代理、选择哪个事务管理器,业务和 outbox 是否使用同一事务资源。
- 在外层事务调用业务方法,待内层返回后主动抛错;用独立连接检查两类记录均未提交,提交后监听未执行。
- 正常提交一组,再检查监听阶段与待办可见性;模拟提交后立即退出,验证重启扫描可以恢复。
- 分别覆盖受检异常、rollback-only 和重复请求,核对数据库状态、响应与幂等结果,而非只看方法内日志。
Spring TestContext 中带 @Transactional 的测试默认在结束时回滚,因此方法内查询到记录不能证明已提交,提交后监听也可能从未执行。需要在测试方法内检查提交结果时,可先调用 TestTransaction.flagForCommit(),再用 TestTransaction.end() 显式结束测试事务,随后断言监听结果并通过独立连接检查持久化状态;只标记提交而不结束事务,断言仍然早于提交。也可让测试入口不带事务,由业务代理完成提交后再断言。真实提交的数据需自行清理,参见 Spring 测试事务文档。以上仍是验证建议,本文未运行这些测试。相邻阅读:原站 Spring 问答、缓存失效补偿。学习资料统一进入 资料入口。
