文章目录
目的:有时候一个业务调用链的场景很长,其中调了各种各样的方法,现在需要把同一次业务调用链上的日志串起来,也就是给同一个 trace ID 即可。
分析及实现过程:
什么是MDC
1、MDC(映射诊断上下文)是一个把键值对(Map<String,String>
)绑定到当前线程上的机制。主要用于把请求级别或线程级别的上下文信息(如 traceId、userId等)自动带到日志输出中,便于日志追踪与排查。它是 SLF4J
提供的 API。
2、MDC 在大多数实现里基于 ThreadLocal<Map<String,String>>
。也就是说,MDC 的数据绑定到 线程 上,只有当前线程能看到自己的 MDC。
3、大多数 Spring Boot 项目默认已经包含:spring-boot-starter-logging
(内部包含 Logback 和 slf4j),因此通常无需额外引依赖。

使用:MDC.put("TRACE_ID", "xxx")
,则在日志格式里用 %X{TRACE_ID}
(Logback)或 ${ctx:TRACE_ID}
(Log4j2)取值。
常用API
常见方法(都来自 org.slf4j.MDC
):
MDC.put(String key, String value)
:设置键值对到当前线程 MDC。MDC.get(String key)
:获取当前线程中 key 对应的值。MDC.remove(String key)
:移除某个 key。MDC.clear()
:清空当前线程全部 MDC。MDC.getCopyOfContextMap()
:获取当前线程 MDC 的 拷贝 (返回Map<String,String>
或null
)。用于把父线程上下文传给子线程。MDC.setContextMap(Map<String,String>)
:把给定 map 设为当前线程的 MDC(覆盖)。
注意:尽量使用 getCopyOfContextMap()
获取拷贝,避免直接传递可变引用导致并发修改问题
代码实现
可以参考上传到 Gitee 中的代码:完整代码
下面贴出主要代码,并附带分析:
1、再来看看主要的实现代码
1.1 先自定义线程池,并配置线程池
java
/**
* 自定义ThreadPoolTaskExecutor线程池
*/
public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
// 重写父类的方法,进行任务包装:为了将父线程的MDC传进去线程池
@Override
public void execute(Runnable task) {
super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
}
java
/**
* 线程池配置
*/
@Configuration
public class MyThreadPoolTaskExecutorConfig {
@Bean("MyExecutor")
public Executor asyncExecutor() {
MyThreadPoolTaskExecutor myExecutor = new MyThreadPoolTaskExecutor();
myExecutor.setCorePoolSize(5);
myExecutor.setMaxPoolSize(5);
myExecutor.setQueueCapacity(500);
myExecutor.setKeepAliveSeconds(60);
myExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
myExecutor.setThreadNamePrefix("ZFP");
myExecutor.initialize();
return myExecutor;
}
}
1.2 生成并使用 traceId
java
public final class ThreadMdcUtil {
private static final String TRACE_ID = "TRACE_ID";
// 生成唯一标识
public static String generateTraceId() {
return UUID.randomUUID().toString();
}
// 假如当前线程没有traceId,就生成一个并设置
public static void setTraceIdIfAbsent() {
if (MDC.get(TRACE_ID) == null) {
MDC.put(TRACE_ID, generateTraceId());
}
}
/**
* Callable(异步任务有返回值):父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*/
public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
return () -> {
// 如果父线程传进来的(他的MDC)没有值
if (context == null) {
// 清理子线程的MDC,为下一步做准备
MDC.clear();
} else {
// 有值:父线程的MDC复制给子线程
MDC.setContextMap(context);
}
// context!=null,并且context里面有traceId,
// 上面else不是做了,这里为什么还做,为了确保有TRACE_ID,兜底
setTraceIdIfAbsent();
try {
return callable.call();
} finally {
MDC.clear();
}
};
}
/**
* Runnable(异步任务没有返回值):父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*/
public static <T> Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
分析代码:
① 父线程 和 子线程
- 父线程(调用方):当前正在运行的线程,比如处理HTTP请求的线程、主线程。它决定向线程池提交任务。
- 子线程(执行方):真正在线程池中执行任务的线程。它不是新建的,而是线程池里复用的工作线程。
父线程是"派任务的人",子线程是"干活的人"。父线程要告诉子线程:"你干活时要带上我的上下文(trace id、userId 等)"
② 为什么要重写线程池中的提交和执行方法?
如果不重新自定义线程池,子线程看不到父线程的MDC(也就是父线程的MDC怎么传给子线程),比如下图:

图中:执行 /insert
接口,流程:主线程在拦截器中设置了 MDC ----> 遇到 @Async
注解的方法,由于没有指定线程池,所以使用默认的线程池执行 ----> 子线程肯定拿不到父线程的 MDC,所以主线程有MDC,子线程为空。
所以要重写线程池中的方法,以便将父线程的 MDC 传给子线程。那怎么传?看 ③ 、④
③ 为什么要写这个 wrap 方法?
- MDC是基于ThreadLocal的 ----> 所以父线程和子线程各自有独立的 MDC 存储空间 ----> 所以子线程默认 拿不到父线程的 MDC 内容。
- 而且如果线程池复用线程,还可能"串号"(也就是:子线程可能残留上一次任务的 MDC)
④ 那怎么理解 wrap 方法?
-
父线程调用 wrap 方法(创建包装);子线程执行包装内部的代码;目的是 把父线程的 MDC 上下文(比如 traceId)传递给子线程使用。
-
代码解释:
-
如果父线程没传任何上下文(
context == null
),那清空子线程的 MDC(为下一步做准备)----> 否则,把父线程的上下文设置到子线程;不管哪种情况,最后都执行一次
setTraceIdIfAbsent()
。 -
当父线程
context
不为null
,MDC.setContextMap(context);
那为什么还需要setTraceIdIfAbsent();
已经重复了 ,为什么?- 如果
context
已经包含了"TRACE_ID"
,那MDC.setContextMap(context)
后,MDC 里肯定已经有这个 key。 这时再调用setTraceIdIfAbsent()
,它检查到MDC.get("TRACE_ID") != null
,就不会再生成新的 traceId。也就是说它不会"重复设置",但会进行一次判断。 - 兜底检查:
- 假如父线程 context 内容为:
{ "USER_ID": "u001" }
(父线程没 TRACE_ID),子线程setTraceIdIfAbsent()
会生成一个新的 traceId(兜底); - 假如无任何 context(父线程没传 MDC),
MDC.clear()
后执行setTraceIdIfAbsent()
,生成一个新的 traceId
- 假如父线程 context 内容为:
总结:
setTraceIdIfAbsent()
的存在是为了"防御"那些没有带 traceId 的任务场景,保证所有线程都有唯一标识。(虽然在拦截器中设置了,但防止意外) - 如果
-
5、运行验证:
① 正常没有异步任务的接口:/pay

② 有异步任务的接口:/insert

本来是正常的结果,但是有个问题:就是正常实现了,而且我没指定线程池,它自己就使用了我的线程池,按道理来说,应该使用默认的线程池(SimpleAsyncTaskExecutor
,然后实现不了)

再谈@Async
1、当 Spring 处理 @Async
时:如果容器里存在一个(或可识别的)Executor/TaskExecutor
bean,Spring 会把它当作默认执行器来使用 。所以你"没显式在 @Async
指定线程池"也会用到你定义的线程池 ------ 因为 Spring 自动选了容器里那个合适的 Executor 作为默认。
2、@Async
的执行器来源优先级:
- 如果
@EnableAsync
的配置类实现了AsyncConfigurer
,则getAsyncExecutor()
返回的Executor
会被当作默认 executor。否则,若容器中有唯一的Executor
/TaskExecutor
类型的 bean,Spring 会把它当默认 executor。 - 若容器中有多个
Executor
,但有一个名为taskExecutor
(或有@Primary
)的 bean,则会优先使用它。 - 最后兜底:如果没有找到可用的 executor,Spring 会创建一个
SimpleAsyncTaskExecutor
作为默认(但这通常不是你遇到的情况)。
3、定位是哪一个 Executor
java
@Component
public class ExecutorInspector {
@Autowired
private ApplicationContext ctx;
@PostConstruct
public void inspect() {
Map<String, Executor> execs = ctx.getBeansOfType(Executor.class);
System.out.println("===== Executors in context =====");
execs.forEach((name, exe) -> {
System.out.println("beanName=" + name + ", class=" + exe.getClass().getName());
});
System.out.println("================================");
}
}

4、怎么灵活选择:
java
@Configuration
public class MyThreadPoolTaskExecutorConfig implements AsyncConfigurer { // 实现这个接口
@Bean("MyExecutor")
public Executor asyncExecutor() {
MyThreadPoolTaskExecutor myExecutor = new MyThreadPoolTaskExecutor();
myExecutor.setCorePoolSize(5);
myExecutor.setMaxPoolSize(5);
myExecutor.setQueueCapacity(500);
myExecutor.setKeepAliveSeconds(60);
myExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
myExecutor.setThreadNamePrefix("ZFP");
myExecutor.initialize();
return myExecutor;
}
// 默认的执行器
@Override
public Executor getAsyncExecutor() {
return new SimpleAsyncTaskExecutor();
}
}

结束,记得点赞收藏哦!!!