注解写了,事务没了
老王排查一单「订单创建了但资金流水没落」的 bug,代码里 @Transactional 明明写着。追了半天,原因是同类内部调用——事务方法被另一个方法 this 调用,代理被绕过,注解形同虚设。这类事故太常见,他把 Spring 事务的翻车姿势归成七宗罪,逐条过。
前四宗:事务静默失效
罪一:方法不是 public。Spring 事务靠 AOP 代理实现,代理只增强 public 方法——protected、private 方法上的 @Transactional 直接被忽略,且没有任何报错,静默失效。
罪二:同类自调用。this.xxx() 调用走的是对象本体不是代理,事务不生效。要么把方法拆到另一个 Bean,要么注入自己的代理(或用 AopContext.currentProxy),最稳的是拆分。
罪三:异常被吞。事务方法里 try-catch 把异常吃掉了,事务管理器看不见异常,照常提交——回滚靠的是异常冒泡到代理层。要么让异常飞出去,要么 catch 后手动 setRollbackOnly。
罪四:rollbackFor 没配。默认只对 RuntimeException 和 Error 回滚,受检异常(IOException 之类)默认照常提交——业务代码里抛个受检异常,数据照样落库。金融类方法养成习惯:@Transactional(rollbackFor = Exception.class)。
后三宗:事务生效了,但干错了事
罪五:事务里发 MQ。消息在事务提交前就发出去了,事务随后回滚——消费者收到了一笔「不存在的业务」。这正是第 7 篇两难问题的错误答案,解法要么本地消息表/事务消息,要么用 TransactionSynchronization 的 afterCommit 钩子,等提交确实完成再发。
罪六:事务里调远程接口。RPC、HTTP、第三方 API 塞进事务方法——数据库连接和行锁被按住等网络响应,一次下游抖动就是连接池耗尽(第 14 篇拖垮连接池的同款病)。远程调用一律移出事务,用落状态位的方式把「做了什么」先记下来。
罪七:传播行为用错。内层 REQUIRES_NEW 挂起外层事务另开新事务——外层最终回滚时,内层已提交的数据不跟着回,留下幽灵数据;嵌套长链里这种情况极难排查。传播行为是精密工具,默认 REQUIRED 就好,REQUIRES_NEW 必须想清楚「这段提交是否真的独立于外层生死」再用。
正确姿势:小而纯粹
七宗罪的病理汇总成一条设计准则:事务边界只包住纯数据库操作,越小越好。校验、远程调用、消息、耗时计算全部放在事务外,靠「先落状态、再干脏活、结果回写」的结构衔接。一个健康的事务方法长这样:进方法即开事务,几条 SQL,立即提交,全程毫秒级。凡是注定要跨网络、跨进程的业务,用前面九篇的模式去组织,别让 @Transactional 承担它管不到的范围。
自查清单
老王把七宗罪压成五问进了 code review 模板:一问 事务方法是否 public 且被外部 Bean 调用;二问 异常能否冒泡、rollbackFor 是否配全;三问 事务内有没有 MQ/RPC/HTTP;四问 事务内有没有循环查库或大数据处理;五问 传播行为是否是默认 REQUIRED。五问过完,Spring 事务的坑就堵住了八成。
小结
事务边界一句话:失效四坑(非 public、自调用、吞异常、漏 rollbackFor)靠纪律,滥用三坑(发消息、调远程、跑重活)靠结构;事务边界小而纯粹,脏活留给模式。到这一篇,主动防御都讲完了。但现实是:无论防御多严密,差异总会发生——最后一道防线叫对账,下一篇见。
评论 (0)