默认ThreadFactory无法传递MDC或业务ID,因其创建的线程不继承父线程的ThreadLocal值;MDC基于ThreadLocal实现,子线程不会自动复制上下文,导致异步日志中traceId等信息丢失。
为什么默认 ThreadFactory 无法传递 MDC 或业务 ID
ExecutorService 默认使用的
(如
)创建的线程是“干净”的:不继承父线程的
(Mapped Diagnostic Context),也不携带任何业务上下文 ID。一旦任务被提交到线程池,
在新线程里就是
,日志链路断裂,排查困难。
根本原因在于:MDC 是基于
实现的,而子线程不会自动复制父线程的
值 —— 即使你在线程提交前设置了
,池中线程执行时也看不到它。
如何用自定义 ThreadFactory 复制 MDC 和业务 ID
关键是在创建线程时,把当前线程的 MDC 快照“冻结”并绑定到新线程的
上。注意不是直接引用,而是深拷贝(
),否则仍会受父线程后续修改影响。
实操建议:
在提交任务前,确保当前线程已设置好
或其他业务键(如
、
)
自定义
中重写
,在新建线程的
执行前注入上下文
使用
或构造时手动
务必在任务执行完毕后清理 MDC(推荐用 try-finally 或装饰 Runnable),避免线程复用污染
示例片段:
Runnable 装饰器比 ThreadFactory 更可靠
仅靠
注入上下文有明显缺陷:它只在**线程创建时**生效,而线程池会复用线程。如果一个线程先执行带 traceId 的任务 A,再执行无上下文的任务 B,B 就可能意外继承 A 的 MDC。
更稳妥的做法是:不依赖线程级绑定,而是在任务调度时显式封装上下文。即用装饰器包装
或
:
构造装饰器时捕获当前
在
开头
,结尾
即使线程复用,每次执行都是独立上下文
与
可叠加使用,但装饰器是兜底手段
典型实现:
Spring 环境下别漏掉 @Async + ThreadPoolTaskExecutor 的配置点
如果你用的是 Spring 的
,底层默认
并不自动传播 MDC —— 即使你配了自定义
,也得配合
和装饰逻辑才能稳定工作。
必须检查的三个地方:
是否设为你带 MDC 拷贝能力的工厂
是否设置了上下文复制逻辑(Spring 4.2+ 支持,比手动包装
更轻量)
是否在
方法内或其调用链中提前清空了 MDC(比如某些中间件 auto-clear)
推荐用
:
真正麻烦的从来不是“怎么加 traceId”,而是线程复用场景下上下文残留、异步嵌套时的覆盖顺序、以及不同框架(Logback/Log4j2/Spring Boot Actuator)对 MDC 生命周期的隐式干预 —— 这些细节不验证到日志输出,很容易以为“已经好了”。
ThreadFactoryExecutors.defaultThreadFactory()MDCMDC.get("traceId")nullThreadLocalThreadLocalMDC.put("traceId", "abc123")ThreadLocalMDC.getCopyOfContextMap()MDC.put("traceId", ...)"tenantId""userId"ThreadFactorynewThread(Runnable r)run()ThreadLocal.withInitial(() -> ...)MDC.setContextMap(...)ThreadFactory factory = r -> {
Thread t = new Thread(r);
// 拷贝当前线程的 MDC 到新线程
Map parentMdc = MDC.getCopyOfContextMap();
t.setUncaughtExceptionHandler((th, ex) -> {
if (parentMdc != null) MDC.setContextMap(parentMdc);
log.error("Uncaught exception in thread {}", th.getName(), ex);
MDC.clear();
});
return t;
}; ThreadFactoryRunnableCallableMDC.getCopyOfContextMap()run()MDC.setContextMap(copy)MDC.clear()ThreadFactorypublic class MdcPreservingRunnable implements Runnable {
private final Runnable delegate;
private final Map mdcContext;
public MdcPreservingRunnable(Runnable delegate) {
this.delegate = delegate;
this.mdcContext = MDC.getCopyOfContextMap();
}
@Override
public void run() {
Map old = MDC.getCopyOfContextMap();
if (mdcContext != null) MDC.setContextMap(mdcContext);
try {
delegate.run();
} finally {
MDC.clear();
if (old != null) MDC.setContextMap(old); // 可选:恢复调用方上下文
}
}
} @AsyncThreadPoolTaskExecutorThreadFactoryAsyncUncaughtExceptionHandlersetThreadFactory()setTaskDecorator()Runnable@AsynctaskDecoratorexecutor.setTaskDecorator(r -> {
Map context = MDC.getCopyOfContextMap();
return () -> {
if (context != null) MDC.setContextMap(context);
try {
r.run();
} finally {
MDC.clear();
}
};
}); 