💡 一则也许对你有用的小广告 🏆

欢迎飞飞程序员   ,你将获得:专属的实战项目(已更新的所有会员标识的项目都能学习) / 1v1 提问 / Java 学习路线 / PHP 学习路线 / 学习打卡 / 社群讨论

  • 正在进行中的项目:《FFBlog知识付费博客项目》 正在持续更新中,基于 Spring Boot 3.x + JDK 21...,点击查看 ;
  • 《从零开发:FFBlog知识付费博客项目(全栈开发)》 演示链接: https://ffblog.ffcxy.com/  ;

截止目前, 飞飞  正在疯狂爆肝实战项目,后续还会上新更多项目,目标是将所学知识开发成项目并且分享给大家,如知识付费系统, Ai系统, CMS系统,在线商城系统,等等 ,欢迎点击围观

一句话结论:MDC(Mapped Diagnostic Context)是 SLF4J/Logback 提供的线程级“上下文存储”,用 ThreadLocal 把 traceId、userId 等字段绑定到当前线程,日志模板里用 %X{key} 就能自动输出,从而在成千上万行日志中串起同一次请求。

背景:多线程日志为什么难串

一个 Spring Boot 应用里,一次 HTTP 请求会经过 Controller → Service → DAO,可能还会丢进线程池、发 MQ、调下游 RPC。日志是并行刷的,默认格式长这样:

2024-05-20 10:00:01 INFO  OrderService - 开始创建订单
2024-05-20 10:00:01 INFO  PayService   - 扣款成功
2024-05-20 10:00:01 INFO  OrderService - 创建订单完成

问题很明显:

  • 同一时刻多个用户的日志交织在一起,无法区分哪几行属于同一次请求。
  • 出问题时要靠时间戳和肉眼猜,链路一长就废。
  • 想在每条日志里带上 traceId,又不想给每个方法都加参数(侵入性太大)。

MDC 就是解决这个的:不改方法签名,也能让日志自动带上下文。

核心结构:MDC 与线程的关系

图中关键点:

  1. 请求入口(Filter/Interceptor)把 traceId、userId 放进 MDC。
  2. MDC 底层是 ThreadLocal<Map<String,String>>,只对当前线程可见。
  3. 业务代码里任何一次 log.info(...),Logback 通过 %X{traceId} 从当前线程的 MDC 取值。
  4. 请求结束必须 MDC.clear(),否则线程复用会串号。

关键步骤与配置

1. 日志格式里声明占位符

logback-spring.xml 或 application.yml:

logging:
  pattern:
    console: "%d{HH:mm:ss.SSS} [%thread] [%X{traceId:-}] [%X{userId:-}] %-5level %logger{36} - %msg%n"

%X{traceId:-} 中的 :- 表示 key 不存在时输出空串,避免打出 traceId_IS_UNDEFINED。

2. 在入口写入 MDC

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MdcFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse resp,
                                    FilterChain chain) throws ServletException, IOException {
        String traceId = req.getHeader("X-Trace-Id");
        if (traceId == null || traceId.isBlank()) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }
        try {
            MDC.put("traceId", traceId);
            MDC.put("userId", currentUserId(req));
            resp.setHeader("X-Trace-Id", traceId);
            chain.doFilter(req, resp);
        } finally {
            MDC.clear();   // 必须清理,防止线程池串号
        }
    }
}

3. 异步 / 线程池:MDC 会丢

这是最常见的坑。ThreadLocal 不会自动传递给子线程,@Async、CompletableFuture、ExecutorService 里拿到的 MDC 是空的。

方案一,手动拷贝:

Map<String, String> ctx = MDC.getCopyOfContextMap();
executor.submit(() -> {
    if (ctx != null) MDC.setContextMap(ctx);
    try {
        doSomething();
    } finally {
        MDC.clear();
    }
});

方案二,包装线程池(推荐):

public class MdcTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        Map<String, String> ctx = MDC.getCopyOfContextMap();
        return () -> {
            if (ctx != null) MDC.setContextMap(ctx);
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
        exec.setCorePoolSize(8);
        exec.setMaxPoolSize(32);
        exec.setTaskDecorator(new MdcTaskDecorator());
        exec.initialize();
        return exec;
    }
}

TaskDecorator 是 Spring 提供的钩子,@Async 线程池会自动应用它。

4. 一次请求的时序

对比:MDC vs 其它方案

方案 侵入性 跨线程 跨服务 适用场景
方法参数透传 traceId 高 手动 手动 不推荐,签名污染
MDC + ThreadLocal 低 需装饰器 需透传 Header 单体 / 单线程链路
MDC + Sleuth/Micrometer Tracing 低 自动 自动(W3C/B3) 微服务全链路追踪
结构化日志(JSON + 字段) 低 同 MDC 同 MDC 接 ELK/Loki 检索

要点:MDC 负责“本进程内上下文”,跨进程要靠 Header 透传,Sleuth / Micrometer Tracing 就是在 MDC 之上补了自动透传和采样。

注意点 / 常见坑

  1. 必须 clear():Tomcat 线程是复用的,不清理下一次请求会带上一次残留的 traceId。
  2. 线程池会丢:@Async、CompletableFuture、parallelStream、new Thread 都不继承 MDC,用 TaskDecorator 或手动 getCopyOfContextMap。
  3. 响应式(WebFlux)不适用:Reactor 一个请求会在多个线程间切换,MDC 的 ThreadLocal 模型失效,要用 Hooks.onEachOperator 或 Reactor Context。
  4. %X 取不到时:如果一次请求没经过 Filter,MDC 是空的,格式里用 %X{traceId:-} 兜底。
  5. 不要塞大对象:MDC 只是 Map<String,String>,放 JSON、堆栈会污染日志并占用内存。
  6. MDC 不参与日志级别过滤:%X 只是渲染值,若想按 traceId 动态改级别,得用 Logback 的 TurboFilter 或 MDCBasedDiscriminator。

小结

MDC 的本质是 SLF4J 暴露的线程级 KV 上下文,配一个 %X{key} 模板就能让日志自动带上 traceId、userId,几乎零侵入。它的边界也很清晰:只在一个线程内有效,只在单个进程内有效。跨线程用 TaskDecorator 补齐,跨服务靠 HTTP Header 透传,再往上就是 Sleuth / Micrometer Tracing 那套全链路方案。理解了 ThreadLocal 这一层,异步丢上下文、响应式失效这些“异常”就不难解释了。

上一篇 手把手教你用 AOP 实现 API 请求日志(自定义注解版)
下一篇 MDC 是什么?一文搞懂日志上下文(traceId)的实现原理

全部评论(0)

头像
😃 😁 😅 😂 😍 😜 😝 🤑 🥵 🥰 😙 😎 😵 😭 😱 😖 🥳 👽 🙈 🤡 😤 💣 💯 💢 ❤️ 👍 👏 👋 👌 🤏 🙏
还没有任何评论哟~
微信扫一扫
微信二维码
微信客服
QQ 咨询
QQ二维码
工作日 9:00 - 18:00 在线
QQ客服
在线工单
提交问题工单,我们会尽快回复处理。
提交工单
在线工单