代码上写了 @Transactional,数据库却留下了本应撤销的数据。排查时先确认事务有没有生效,再确认异常是否触发回滚,最后看实际操作是否属于同一个事务。
先查调用有没有经过代理
Spring 默认代理模式会拦截从外部经过代理进入的方法调用。同一个对象内部直接调用另一个带注解的方法,不会因为那个方法上的注解而自动新增事务拦截。
假设 createOrder 在类内部直接调用 saveOrder,只有 saveOrder 标了注解。需要检查入口是否通过代理,以及当前是否已经有其他事务。看到注解不能推断这一次调用受到了它的管理。Spring 官方说明明确说明了自调用的限制。
自己 new 出来的对象,同样需要检查是否脱离容器管理。方法可见性还受代理类型与版本影响。Spring 6 的类代理可支持部分非 public 方法,接口代理有其要求,不能把"所有非 public 方法都失效"当成通用结论。
异常有没有传到事务拦截器
传统默认规则下,RuntimeException 与 Error 触发回滚,普通受检异常不会自动触发。可以按业务设置 rollbackFor。Spring 6.2 起还支持调整全局默认回滚策略,因此需要查看当前项目的真实配置。
假设数据库操作之后抛出了异常,但方法内部 catch 住并正常返回。代理可能看不到应触发回滚的异常。仅打印日志,无法表达业务希望回滚的决定。
处理方式应与业务语义一致,需要回滚就通过合适的异常或回滚标记表达,同时避免把应该正常处理的业务状态全部转成异常。
传播行为决定共享哪些事务
REQUIRED 在已有事务中通常参与同一个物理事务。内部参与者设置 rollback-only 后,外层即使捕获异常继续执行,也可能在最终提交时收到 UnexpectedRollbackException。
REQUIRES_NEW 使用独立事务。内层已提交的记录,不会因为外层随后回滚而自动撤销。外层占着连接再申请内层连接,还需要考虑连接池容量与并发压力。
NESTED 通常依赖保存点以及事务管理器和资源的支持。不能仅凭注解名称,就认为所有数据访问技术都能按同样方式回滚内层操作。
这些规则可以进一步核对传播行为文档。
本地事务管理哪些资源
数据库回滚不会撤回已经发送的邮件,也不一定取消外部 HTTP 请求。如果订单事务里调用了外部支付接口,数据库失败以后,外部扣款结果必须单独核对。
这类流程需要明确幂等、状态核对与补偿安排。把全部调用放在带注解的方法中,不会自动让它们成为一个跨系统原子操作。
验证时通过真实服务入口调用,准备异常发生在写入前后、异常被捕获、独立内层提交等场景,再检查最终数据库状态。测试自身包裹的事务可能影响观察,需要确认验证的是生产调用方式。
沿着调用入口、异常处理和资源范围逐步检查,通常比反复增删注解更容易找到原因。
我是程序员Sunday,本文相关的完整解析收录在 sunday面试指南,可继续阅读《Spring 的 @Transactional 为什么会失效?事务传播行为怎么选?》。