MDC(Mapped Diagnostic Context)链路追踪实战
请详细解释SLF4J的**MDC(Mapped Diagnostic Context)**是什么?如何使用MDC实现请求级别的链路追踪(TraceId传递)?在异步线程和多线程池场景下,MDC如何正确传递?MDC的底层原理是什么?
回答
古法程序员
MDC是什么
MDC(Mapped Diagnostic Context)是SLF4J提供的线程绑定的键值对上下文,用于在日志输出中携带追踪信息(如TraceId、UserId、RequestId)。
// 设置MDC
MDC.put("traceId", UUID.randomUUID().toString());
MDC.put("userId", "12345");
// 日志配置中使用
// %X{traceId} 或 %X(所有MDC)
<pattern>%d [%thread] %X{traceId} %-5level %logger - %msg%n</pattern>
// 输出示例:
// 2025-01-01 12:00:00 [http-nio-8080] abc-123 INFO c.e.UserService - 查询用户
请求级别传递(Filter拦截器)
@Component
public class TraceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
try {
MDC.put("traceId", UUID.randomUUID().toString().substring(0, 8));
MDC.put("uri", ((HttpServletRequest) request).getRequestURI());
chain.doFilter(request, response);
} finally {
MDC.clear(); // 必须清理,防止内存泄漏
}
}
}
异步线程MDC传递
MDC基于ThreadLocal,异步线程中需要手动传递。
// 方案1:包装Runnable
public class MdcRunnable implements Runnable {
private final Runnable task;
private final Map<String, String> contextMap = MDC.getCopyOfContextMap();
public MdcRunnable(Runnable task) {
this.task = task;
}
@Override
public void run() {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
try {
task.run();
} finally {
MDC.clear();
}
}
}
// 方案2:Spring @Async
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(runnable -> {
Map<String, String> ctx = MDC.getCopyOfContextMap();
return () -> {
if (ctx != null) MDC.setContextMap(ctx);
try { runnable.run(); } finally { MDC.clear(); }
};
});
return executor;
}
底层原理
- MDC内部使用
ThreadLocal<Map<String, String>>存储 - 每个线程独立维护一份MDC副本
- Logback/Log4j2的PatternLayout解析
%X{key}时从当前线程MDC获取值 MDC.clear()防止线程池复用线程时数据泄漏