@Async嵌套@Transactional引发静默故障:从竞态条件到事务回调的修复全记录
引言:一个没有报错的线上故障
上周我们在灰度环境压测订单履约模块时,遇到一个非常隐蔽的线上问题:约5%的订单绑定了错误的履约关系,服务日志里没有任何异常报错,属于典型的“静默失败”。排查了两天最终定位到根因:@Async 异步方法被直接放在 @Transactional 事务方法内部调用,异步线程无法读取到主线程事务未提交的关联数据,最终导致绑定逻辑失效。
问题根因:@Async与@Transactional嵌套的经典竞态陷阱
Spring 事务的默认传播机制要求事务在方法执行完成、返回前才会提交数据,而 @Async 注解会新开独立线程执行异步逻辑,两个线程之间默认不共享事务上下文。
在我们的场景中,主线程执行业务逻辑、修改关联数据后,直接调用了异步方法去读取刚修改的关联数据做绑定。此时主线程的事务还未提交,异步线程拿不到未提交的关联数据,读出来的是空值或者旧值,最终绑定逻辑走到错误分支,生成错误的绑定关系。更麻烦的是,这个逻辑没有抛出明确异常,也没有做空值校验,所以完全静默失败,直到灰度用户反馈才被发现。
修复方案:用事务提交回调规避竞态
最初我们考虑过两种方案:一是给异步读逻辑加分布式锁,保证数据一致性,但会大幅降低系统吞吐量;二是把异步逻辑改成同步执行,但会影响接口响应时间。最终我们选定了Spring原生提供的事务回调方案,在不影响性能的前提下解决竞态问题。
具体修复逻辑在 FpProcessStepServiceImpl.java 中实现:
- 新增
TransactionSynchronization和TransactionSynchronizationManager的导入,将原本直接调用的异步逻辑封装到afterCommit回调中,确保主线程事务完全提交后,再触发异步线程执行任务,此时异步线程可以正常读取到已提交的关联数据。 - 补充边界降级逻辑:考虑到部分场景可能不存在事务上下文(比如方法被非事务方法调用),我们新增了判断逻辑:当
TransactionSynchronizationManager.isSynchronizationActive()返回false时,直接执行异步调用,同时打印warn日志提示无事务上下文,避免任务完全不执行。
修复效果与上线前待办
修复后代码编译通过,故障等级评定得分从90分提升至95分,该故障的风险等级从高降低为低。目前剩余上线前必须完成的两项工作:一是将 application.yml 中的 spring.profiles.active 配置修改为 prod,二是执行高负载性能测试验证接口响应时间(RT)指标是否符合要求。
可带走的方法论
这次故障排查给我们留下了三个可复用的经验:
- 事务内异步逻辑优先用提交回调:凡是需要在事务修改数据后再执行异步逻辑的场景,绝对不要直接在
@Transactional方法内调用@Async方法,优先用TransactionSynchronization.afterCommit回调,既保证数据一致性,又不会阻塞主线程。 - 异步边界必须做降级处理:不要默认所有调用场景都存在事务上下文,针对无事务的场景做好降级逻辑,同时打日志留痕,避免任务丢失。
- 静默失败要额外埋点校验:对于没有明确异常报错的业务逻辑,要补充业务维度的埋点校验,不能只依赖异常日志排查问题,很多隐蔽故障都是“静默失败”导致的。

浙公网安备 33010602011771号