MDC(Mapped Diagnostic Context)
MDC(Mapped Diagnostic Context)是 SLF4J/Logback 提供的线程级日志上下文机制。它允许你在同一个线程中,把关键信息(如 TraceID、UserID)放入上下文中,后续所有日志都会自动带上这些信息。
MDC 是可观测性关联的基础组件之一——没有 MDC,结构化日志就无法包含请求级别的上下文。
MDC 的工作原理
核心概念
MDC 是一个线程本地存储(ThreadLocal)的 Map。每个线程有自己独立的 MDC,数据不会在多线程间泄露。
基本操作
MDC 与 LogstashEncoder 的集成
LogstashEncoder 会自动从 MDC 中提取所有键值对,附加到 JSON 日志中:
logback-spring.xml
输出的 JSON 日志:
典型使用场景
场景一:Web 请求上下文
TraceIdFilter.java
场景二:用户上下文
UserContextFilter.java
场景三:多租户上下文
TenantContextFilter.java
场景四:业务操作上下文
OrderService.java
MDC 传递问题
问题一:异步线程 MDC 丢失
这是 MDC 最常见的问题。线程池提交任务后,新线程没有父线程的 MDC:
解决方案:MDC 工具类
MdcUtils.java
使用方式:
MDC 与 OTel 的关系
MDC 是 Logback 的本地机制,OTel 是跨语言的链路追踪标准。两者需要配合使用:
OTel
OTel Java Agent 会自动将 Span 的 traceId/spanId 同步到 MDC(如果配置了 otel.javaagent.logging.mdc=true)。
常见问题
问题一:MDC 内存泄漏
如果 MDC.clear() 没有在 finally 块中调用,MDC 数据会残留在 ThreadLocal 中,导致内存泄漏。
问题二:ThreadLocal 泄漏
Tomcat 的线程池会复用线程。如果请求结束时没有清理 MDC,下一个请求可能会看到上一个请求的数据。
问题三:父子线程 MDC 不共享
子线程看不到父线程的 MDC。这是 ThreadLocal 的设计特性,不是 bug,但需要通过 MdcUtils.wrap() 显式传递。
质量判断标准
读完本节后,你应该能够回答:
- MDC 的工作原理是什么?它和 ThreadLocal 的关系是什么?
- 为什么说
MDC.clear()应该在 finally 块中调用?如果忘记清理会有什么后果? - MDC 在异步场景下为什么会丢失?如何在异步任务中正确传递 MDC?
- LogstashEncoder 如何从 MDC 中提取数据并包含到 JSON 日志中?
- MDC 和 OTel 的 Context 分别扮演什么角色?它们如何配合使用?