@Async 三大坑:事务丢失、MDC 上下文消失、异常被静默吞掉
@Async 只负责换线程执行,不负责事务一致性、上下文复制和异常可靠投递。 生产环境里因此丢失数据、丢日志、吞异常的情况反复出现,根治只能靠明确事务边界、装饰线程池和持久化关键任务。
事务上下文不会随 @Async 传递到子线程
在 Spring 中,@Async 方法默认使用 SimpleAsyncTaskExecutor 或自定义的 ThreadPoolTaskExecutor 执行。事务由 TransactionSynchronizationManager 绑定在当前线程的 ThreadLocal 上。当主线程调用 @Async 方法时,代理先切换到新线程,新线程的 ThreadLocal 里没有事务上下文,因此 @Async 方法内即使加了 @Transactional 也无法加入已有事务。
实际项目中,一个订单服务在主线程开启事务,调用 @Async 发送积分和推送通知。积分服务里更新用户积分点数因为没有事务,执行后立刻回滚,导致用户积分始终为零。调用方却看不到任何异常,订单状态已标记为成功。
根因在于 @Async 只负责异步执行,不负责事务一致性。Spring 的事务传播机制依赖同一线程,跨线程时 PROPAGATION_REQUIRED 失效,子线程默认使用 PROPAGATION_REQUIRED 但没有父事务可加入,最终变成独立事务或无事务。
调用链上,主线程事务在 @Async 代理返回后立即提交,而子线程的事务边界完全独立。两者没有共享任何资源,这直接导致数据不一致。
MDC 上下文默认不会复制到新线程
MDC(Mapped Diagnostic Context)底层使用 ThreadLocal 存储 traceId、userId 等日志字段。@Async 创建新线程时,ThreadLocal 不会自动复制,因此子线程日志里 traceId 为空,日志平台无法聚合一次请求的所有链路。
某支付项目里,主线程设置 MDC.put(“traceId”, UUID.randomUUID().toString()),随后调用 @Async 做风控校验。风控模块打印的日志全部丢失 traceId,排查问题时无法把用户请求和异步任务关联起来,定位耗时从分钟级变成小时级。
信号明确指出 @Async 不负责上下文复制。ThreadLocal 的设计本身就是线程隔离的,线程池复用线程时旧线程的 MDC 残留还会造成日志错乱。
异常在 @Async 中会被静默吞掉
@Async 方法抛出异常后,默认由 AsyncUncaughtExceptionHandler 处理。这个 handler 的默认实现只是打印堆栈到 error 日志,然后什么都不做。调用方无法通过 try-catch 捕获,也不会向上传播,导致异常被静默吞掉。
一个定时任务异步生成报表,里面因为空指针崩溃。开发者第二天才从日志里发现报表从未生成,业务方已投诉数据缺失。整个过程没有告警,没有重试,任务彻底丢失。
信号指出 @Async 不负责异常可靠投递。默认机制只记录日志,不触发重试,不通知调用方,也不影响主流程状态。
事务边界必须在 @Async 调用前提交
修复事务丢失的核心是把需要事务保护的操作放在 @Async 调用之前完成,确保主线程事务先提交,再发起异步任务。
正确写法是:
|
|
如果异步任务本身也需要事务,应在异步方法上单独标注 @Transactional,让它成为独立事务。不要指望父子事务跨越线程。
生产中还可使用 TransactionTemplate 在明确位置手动提交,再调用异步方法,避免隐式边界带来的困惑。
用装饰器统一包装线程池复制上下文
一次性解决 MDC 和其他 ThreadLocal 丢失的最干净方式是自定义 TaskDecorator。
|
|
然后在配置线程池时注入:
|
|
这样所有 @Async 方法都会自动携带 MDC 上下文,同时也为后续扩展其他 ThreadLocal 留出空间。信号中提到的“统一线程池装饰”正是这一做法。
关键任务持久化加可观察异常才能兜底
即使上下文和事务都修复,异步任务仍可能因为线程池满、节点重启而丢失。信号建议对关键任务做持久化并配合可观察异常。
做法是:异步方法第一步把任务记录到数据库,状态为 PENDING。执行成功后更新为 SUCCESS,失败则更新为 FAILED 并记录异常堆栈。同时注册自定义 AsyncUncaughtExceptionHandler,把异常写入数据库并触发告警。
|
|
配合定时任务扫描长期 PENDING 的记录进行重试,形成闭环。持久化确保任务不因进程退出而消失,可观察异常则把原来静默的错误变成可追踪、可告警的事件。
避坑 checklist:从代码到运维的检查项
- 所有 @Async 方法是否都在事务外调用,或明确标注独立事务。
- 线程池是否设置了 ContextCopyingDecorator 复制 MDC 和其他上下文。
- 是否替换了默认 AsyncUncaughtExceptionHandler,实现持久化和告警。
- 关键异步任务是否落地到数据库,状态可查询、可重试。
- 线程池核心参数(核心数、最大数、队列容量)是否根据实际压测设定,避免拒绝策略导致任务丢失。
- 日志中是否能通过 traceId 完整串联主流程与所有异步环节。
- 生产环境是否开启异步任务监控指标(活跃线程、队列长度、失败率)。
逐条核对以上清单,可大幅降低 @Async 带来的隐蔽故障。实际项目中,完整实施这套方案后,异步任务相关的事故率从每月 3-5 起降至接近零。
整个修复思路围绕信号给出的四个根治手段展开:明确事务边界、统一线程池装饰、可观察异常、关键任务持久化。它们共同构成生产级异步处理的底线保障。
参考来源
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/geek/post/20260903/Async-%E4%B8%89%E5%A4%A7%E5%9D%91%E4%BA%8B%E5%8A%A1%E4%B8%A2%E5%A4%B1MDC-%E4%B8%8A%E4%B8%8B%E6%96%87%E6%B6%88%E5%A4%B1%E5%BC%82%E5%B8%B8%E8%A2%AB%E9%9D%99%E9%BB%98%E5%90%9E%E6%8E%89/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com