Skip to content

Spring 事务的代理、自调用、回滚规则和提交后动作有哪些关键陷阱?

题型:深入 · 从代理拦截链解释“看起来有注解但没有事务”的原因。

建议回答

Spring 声明式事务通常在代理边界生效。外部调用先进入代理,事务拦截器解析方法和类上的属性,再调用目标方法;同一个对象通过 this 调用另一个事务方法时没有再次经过代理,因此新方法上的传播行为往往不会生效。解决方式优先是重新划分组件边界,让调用经过独立 Bean,而不是到处获取当前代理。

默认情况下,运行时异常和 Error 触发回滚,受检异常通常不回滚,但可以通过 rollbackFor 调整。更隐蔽的问题是异常被业务代码捕获后没有继续抛出,或内层事务已标记 rollback-only,外层仍尝试提交,最终出现 UnexpectedRollbackException。因此要统一异常语义,避免用返回值悄悄吞掉本应回滚的失败。

事务中的“方法返回”不等于所有外部副作用都可靠完成。若在提交前发送消息,数据库随后回滚会产生幽灵消息;若提交后直接发送,进程可能在两者之间宕机。更稳妥的方式是把业务数据和 Outbox 事件写入同一本地事务,再由独立发布器至少一次投递,消费者以业务键幂等。afterCommit 适合非关键通知或触发器,但不能单独弥合宕机窗口。

异步执行也会切换线程上下文,原线程绑定的事务不会自动传播到异步线程。异步任务需要自己的事务边界,并通过稳定 ID 重新读取数据,而不是依赖原事务中的未提交对象状态。

关键权衡

  • 拆分 Bean 能让事务边界明确,但过度拆分会增加调用层级,应以业务原子边界为准。
  • Outbox 提供可恢复的最终一致链路,但会增加事件表、发布器、幂等和清理成本。
  • 扩大回滚范围更符合某些业务语义,却也可能让可恢复的受检异常导致整个长事务重做。

易错点

  • 不是所有 privatefinal 或自调用场景在所有代理模式下都完全相同,回答时应说明常见的代理式 AOP 前提。
  • 不要把 afterCommit 说成可靠消息方案,它仍可能在提交后、回调执行前丢失。
  • 不要认为 @Async 会继承调用线程的数据库事务。

知识关系

从当前问题继续深入,或者回到提出这个问题的知识入口。