💡 一则也许对你有用的小广告 🏆
欢迎飞飞程序员 ,你将获得:专属的实战项目(已更新的所有会员标识的项目都能学习) / 1v1 提问 / Java 学习路线 / PHP 学习路线 / 学习打卡 / 社群讨论
截止目前, 飞飞 正在疯狂爆肝实战项目,后续还会上新更多项目,目标是将所学知识开发成项目并且分享给大家,如知识付费系统, 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(),否则线程复用会导致数据污染。