CodeWalk

MDC(Mapped Diagnostic Context)链路追踪实战

作者:古法程序员 · 2026-05-30 12:55

请详细解释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()防止线程池复用线程时数据泄漏