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

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

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

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

MDC(Mapped Diagnostic Context,映射诊断上下文) 是日志框架(Logback、Log4j 等)提供的一个线程安全的键值对容器,用于在日志中动态添加额外的上下文信息。

简单来说:MDC 就是给每条日志贴上一个"标签",方便你把同一个请求产生的所有日志串联起来。


1. 为什么需要 MDC?

在 Web 应用中,一个请求可能经过 Controller → Service → Mapper 等多个层次,产生多条日志。如果同时有多个用户请求,日志会交错在一起,很难分辨哪些日志属于同一个请求。

没有 MDC 时:

2024-01-01 10:00:01 INFO  UserController - 开始处理请求
2024-01-01 10:00:01 INFO  OrderController - 开始处理请求
2024-01-01 10:00:02 INFO  UserService - 查询用户
2024-01-01 10:00:02 INFO  OrderService - 查询订单
2024-01-01 10:00:03 INFO  UserController - 请求结束
2024-01-01 10:00:03 INFO  OrderController - 请求结束

问题:无法区分哪些日志属于"用户请求",哪些属于"订单请求"。

使用 MDC 后:

2024-01-01 10:00:01 [traceId=aaa] INFO  UserController - 开始处理请求
2024-01-01 10:00:01 [traceId=bbb] INFO  OrderController - 开始处理请求
2024-01-01 10:00:02 [traceId=aaa] INFO  UserService - 查询用户
2024-01-01 10:00:02 [traceId=bbb] INFO  OrderService - 查询订单
2024-01-01 10:00:03 [traceId=aaa] INFO  UserController - 请求结束
2024-01-01 10:00:03 [traceId=bbb] INFO  OrderController - 请求结束

现在可以通过 traceId 轻松筛选出属于同一个请求的所有日志。


2. MDC 的核心原理

MDC 底层使用 ThreadLocal 实现,所以它是线程安全的。

特性 说明
存储结构 Map<String, String> 键值对
底层实现 ThreadLocal<Map<String, String>>
线程隔离 每个线程有自己独立的 MDC 数据,互不干扰
生命周期 与线程绑定,线程结束时不会自动清理,必须手动 clear

关键点:因为每个 HTTP 请求通常由一个独立线程处理,所以 MDC 天然适合用来标记"每个请求"的上下文。


3. MDC 的常用 API

import org.slf4j.MDC;

// 1. 放入键值对
MDC.put("traceId", "abc-123");
MDC.put("userId", "1001");

// 2. 获取值
String traceId = MDC.get("traceId");

// 3. 移除某个键
MDC.remove("traceId");

// 4. 清空所有(重要!)
MDC.clear();

4. 如何在日志中输出 MDC 的值?

需要在 Logback 的日志格式(pattern)中加入 %X{key}。

logback-spring.xml:

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

关键部分:[%X{traceId}]

  • %X{key}:输出 MDC 中指定 key 的值。
  • 如果 MDC 中没有这个 key,则输出空字符串。

输出效果:

2024-01-01 10:00:01.123 [http-nio-8080-exec-1] [abc-123] INFO  UserController - 开始处理请求
2024-01-01 10:00:01.456 [http-nio-8080-exec-1] [abc-123] INFO  UserService - 查询用户

5. 典型使用场景

场景 说明
链路追踪 给每个请求生成唯一 traceId,串联整个调用链
用户标识 把 userId 放入 MDC,日志中自动带上用户信息
租户隔离 多租户系统中,把 tenantId 放入 MDC
灰度发布 把 grayTag 放入 MDC,方便排查问题
异步任务 配合 TaskDecorator 把 MDC 传递到子线程

6. 完整示例(结合前面 AOP 切面)

在切面中设置 MDC:

@Around("apiLogPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    try {
        // 生成 traceId 并放入 MDC
        String traceId = UUID.randomUUID().toString().replace("-", "");
        MDC.put("traceId", traceId);

        log.info("请求开始...");
        Object result = joinPoint.proceed();
        log.info("请求结束...");
        return result;
    } finally {
        // 【重要】必须清理,防止线程复用导致数据污染
        MDC.clear();
    }
}

为什么必须 MDC.clear()?

  • Tomcat 使用线程池,线程会被复用。
  • 如果请求 A 设置了 traceId=aaa,请求结束后没清理,下一个请求 B 复用同一线程时,日志里会错误地带上前一个请求的 traceId=aaa。
  • 所以必须在 finally 中清理。

7. 注意事项

注意事项 说明
必须清理 在 finally 中调用 MDC.clear(),否则线程复用会污染数据
线程隔离 子线程不会自动继承父线程的 MDC,需要手动传递
异步场景 使用 @Async 或线程池时,MDC 会丢失,需要 TaskDecorator 传递
性能开销 MDC 基于 ThreadLocal,开销极小,可放心使用
日志格式 必须在 logback 的 pattern 中加 %X{key} 才能输出

8. MDC vs ThreadLocal

对比项 MDC ThreadLocal
本质 日志框架提供的工具 Java 原生 API
底层 基于 ThreadLocal 本身就是 ThreadLocal
用途 专门用于日志上下文 通用线程隔离
与日志集成 通过 %X{} 直接输出 需要手动写入日志

9. 一句话总结

MDC 就是给每条日志贴上一个"请求标签"(如 traceId),让同一个请求产生的所有日志能串联起来,方便排查问题。 它的底层是 ThreadLocal,所以每个请求互不干扰,但用完必须在 finally 中 clear(),否则线程复用会导致数据污染。

上一篇 SpringBoot中MDC 是什么?
已是最后一篇

全部评论(0)

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