线上 TraceId 集体失踪,大促排查如何破局?
本引用仅用学习,禁用商用
引用自 https://mp.weixin.qq.com/s/fJsYrkBIN6QEK_Xwzsz0eg
近期线上环境出现诡异问题,异步任务里链路 ID(TraceId)莫名丢失,致使核心业务日志断链,严重影响问题排查。今天给大家分享三种有效解决办法 。
1. 事件回顾
3.8 大促期间,我司交易系统流量剧增。在排查问题过程中,我们发现下单主流程的日志出现异常,部分 TraceId 丢失,致使调用链路中断,排查难度急剧上升 。
[2025-03-08 02:15:33] [TID:4a3b...8c2d] INFO 支付校验通过 → 库存扣减成功
// 异常日志片段(TraceId丢失!)
[2025-03-08 02:15:34] [TID:N/A] ERROR 优惠券核销失败
2. 问题定位
通过代码逐层排查,最终锁定“真凶”——一段使用 CompletableFuture 的异步处理代码:
public void processOrder(Order order) {
// 主线程(携带TraceId)
log.info("[主线程] 开始处理订单 {}", order.getId());
CompletableFuture.runAsync(() -> {
// 子线程(TraceId丢失!)
log.info("优惠券核销");
couponService.useCoupon(order.getCouponId());
}, executor);
}
3. 原因分析
根本原因:MDC 依赖 ThreadLocal 实现线程本地存储,每个线程都有独立的上下文存储空间。而线程池复用机制下,子线程被创建时,无法自动继承父线程 ThreadLocal 中的上下文数据,从而引发 TraceId 丢失冲突 。
MDC 实现原理:
- MDC 底层基于 ThreadLocal 实现,为每个线程创建独立的键值存储空间;
- 日志框架通过
%X{traceId}模式从当前线程的ThreadLocal中提取链路ID。
线程池运行机制:
- 线程复用:池化线程完成任务后不会销毁,而是返回池中等待新任务;
- 线程隔离:不同线程持有完全独立的 ThreadLocal 存储空间。
典型问题场景:
public static void main(String[] args) {
// 主线程设置链路ID
ThreadLocal<String> traceIdHolder = new ThreadLocal<>();
traceIdHolder.set("main-tid");
// 子线程无法访问主线程的ThreadLocal
CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + ":" + traceIdHolder.get()); // 输出null
});
System.out.println(Thread.currentThread().getName() + ":" + traceIdHolder.get());
}

4. 解决方案
方案一:手动传递上下文
在提交异步任务时,手动捕获并传递 TraceId,确保子线程能获取到主线程的 TraceId。
public void processOrder(Order order) {
// 主线程(携带TraceId)
log.info("[主线程] 开始处理订单 {}", order.getId());
String tid = MDC.get(TID);
CompletableFuture.runAsync(() -> {
MDC.put(TID,tid);
log.info("[异步任务] 核销优惠券");
couponService.useCoupon(order.getCouponId());
}, executor);
}
这种方式简单直接,不过需要在每个异步任务中手动添加代码,代码侵入性较强,且容易遗漏。
方案二:自定义线程池包装任务
自定义线程池,在提交任务时自动保存当前线程的 MDC 上下文,并在任务执行时恢复,避免手动操作的繁琐。
class MDCTaskDecorator implements Runnable {
privatefinal Runnable delegate;
privatefinal Map<String, String> context;
public MDCTaskDecorator(Runnable delegate, Map<String, String> context) {
this.delegate = delegate;
this.context = context;
}
@Override
public void run() {
Map<String, String> originalContext = MDC.getCopyOfContextMap();
try {
if (context != null) {
MDC.setContextMap(context);
}
delegate.run();
} finally {
if (originalContext != null) {
MDC.setContextMap(originalContext);
} else {
MDC.clear();
}
}
}
}
class MDCTaskExecutor extends ThreadPoolExecutor {
public MDCTaskExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
Map<String, String> context = MDC.getCopyOfContextMap();
super.execute(new MDCTaskDecorator(command, context));
}
}
class CustomThreadPoolSolution {
privatestaticfinal Logger logger = LoggerFactory.getLogger(CustomThreadPoolSolution.class);
public static void main(String[] args) {
MDC.put("trace_id", "654321");
MDCTaskExecutor executor = new MDCTaskExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>()
);
executor.execute(() -> logger.info("异步任务执行,trace_id: {}", MDC.get("trace_id")));
executor.shutdown();
}
}
此方案将上下文传递的逻辑封装在线程池中,对业务代码的侵入性较小,但实现起来相对复杂。
方案三:使用分布式追踪框架
借助分布式追踪框架,如 Skywalking、Zipkin、Pinpoint等,它们能自动为应用程序生成链路 ID,并在多线程、异步调用等场景下正确传递链路 ID,大大简化开发人员在链路追踪方面的操作。
这些框架通过内置的机制,在不同的服务和线程之间自动传递 TraceId,无需手动干预,降低了出错的概率,同时提供了可视化的界面和工具,方便开发人员监控和分析调用链路。
5. 总结
并发工具极大提升了并发代码编写的效率,也预先为潜在问题备好高效解法,是开发过程中的得力助手。
但开发人员不能仅满足于表面应用,务必深入剖析其实现逻辑,明晰不同场景下的适用规则。
若对并发工具一知半解、盲目套用,不仅难以发挥其最大效能,面对复杂问题时会陷入被动,更可能在生产环境中引发严重线上故障。
所以 J.U.C 虽好,可不要贪杯哦!

浙公网安备 33010602011771号