---
URL: "/zh-CN/blog/2026-03/spring-boot-mdc.html"
LLMS_URL: "/zh-CN/blog/2026-03/spring-boot-mdc.md"
lastUpdated: true
commentabled: true
recommended: true
title: "MDC日志链路追踪实战"
description: "让分布式系统问题排查更高效"
date:

pageClass: "blog-page-class"
cover: "/covers/springboot.svg"
---

---
lastUpdated: true
commentabled: true
recommended: true
title: MDC日志链路追踪实战
description: 让分布式系统问题排查更高效
date: 2026-03-19 09:35:00 
pageClass: blog-page-class
cover: /covers/springboot.svg
---

在分布式系统中，一个用户请求往往会经过多个服务、多个线程的处理。当出现问题时，我们面对的是海量的零散日志，如何快速定位某一次请求的完整链路日志？这正是MDC（Mapped Diagnostic Context，映射诊断上下文）要解决的核心问题。MDC通过ThreadLocal为每个线程绑定专属的上下文信息（如请求ID、用户ID），让日志自动携带这些标识，轻松串联全链路日志。今天，我们就从原理到实战，彻底掌握MDC的使用。

## 一、为什么需要MDC？分布式日志的痛点 ##

在单体应用中，日志通常按线程输出，排查问题时只需筛选对应线程ID即可。但分布式系统下，日志分散在多个服务节点，还可能涉及多线程协作（如异步处理、线程池），传统日志排查面临三大痛点：

- 链路断裂：一个请求从网关到业务服务，再到第三方接口，日志分散在不同服务日志文件中，无法通过单一标识串联；
- 定位困难：海量日志中，无法快速区分“哪个日志属于哪个用户/哪个请求”，排查一个问题可能需要筛选几十分钟；
- 参数冗余：为了区分链路，手动在每个日志语句中拼接requestId、userId，代码冗余且易出错。

而MDC的出现，正是通过“线程级上下文绑定”的方式，让日志自动携带链路标识，彻底解决这些问题。无需手动拼接参数，只需一次绑定，全链路日志均可复用上下文信息。

## 二、MDC核心原理：ThreadLocal的巧妙运用 ##

MDC是SLF4J日志框架提供的一个工具类（不同日志实现如Logback、Log4j2均支持），其核心原理是ThreadLocal——为每个线程维护一个独立的Map（上下文容器），存储键值对形式的上下文信息（如requestId: req-123456）。

**核心逻辑**：

- 请求进入时，通过MDC.put(key, value)将链路标识（如requestId）绑定到当前线程的ThreadLocal中；
- 日志输出时，日志框架自动从当前线程的MDC容器中提取键值对，添加到日志内容中；
- 请求结束时，通过MDC.clear()清除当前线程的上下文信息，避免线程复用导致的信息污染。

**MDC核心API（简单易用，仅3个常用方法）**：

```java
// 向当前线程的MDC中添加上下文信息
MDC.put(String key, String value);

// 从当前线程的MDC中获取指定key的上下文信息
MDC.get(String key);

// 清除当前线程的MDC中所有上下文信息（必须在请求结束时调用）
MDC.clear();
```

## 三、Spring Boot中MDC实战：从配置到使用 ##

Spring Boot默认集成SLF4J+Logback，无需额外引入MDC依赖，直接使用即可。下面以“Web请求链路追踪”为例，完整演示MDC的配置与使用流程。

### 第一步：自定义拦截器，绑定链路标识 ###

通过Spring MVC拦截器，在请求进入时生成全局唯一的requestId，绑定到MDC；请求结束时清除MDC。

```java
import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * MDC链路追踪拦截器
 * 功能：生成requestId并绑定到MDC，请求结束后清除MDC
 */
public class MdcTraceInterceptor implements HandlerInterceptor {

    // 定义MDC中requestId的key（统一命名，便于日志配置和检索）
    public static final String REQUEST_ID_KEY = "requestId";
    // 定义MDC中userId的key（用户登录后传递）
    public static final String USER_ID_KEY = "userId";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 生成全局唯一requestId（UUID去除横线，缩短长度）
        String requestId = UUID.randomUUID().toString().replace("-", "");
        // 2. 将requestId绑定到MDC
        MDC.put(REQUEST_ID_KEY, requestId);
        // 3. 从请求头获取userId（用户登录后，前端将userId放入请求头）
        String userId = request.getHeader(USER_ID_KEY);
        if (userId != null && !userId.isEmpty()) {
            MDC.put(USER_ID_KEY, userId);
        }
        // 4. 将requestId放入响应头，便于前端排查（可选，提升排查体验）
        response.setHeader(REQUEST_ID_KEY, requestId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 关键：请求结束后清除MDC，避免线程池复用导致的上下文污染
        MDC.clear();
    }
}
```

### 第二步：配置拦截器，生效所有请求 ###

通过Spring Boot配置类，注册拦截器，让其对所有请求生效。

```java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册MDC拦截器，对所有请求生效
        registry.addInterceptor(new MdcTraceInterceptor())
                .addPathPatterns("/**") // 拦截所有路径
                .excludePathPatterns("/error"); // 排除错误页面路径
    }
}
```

### 第三步：配置日志格式，让日志携带MDC信息 ###

修改logback-spring.xml（或log4j2.xml），在日志格式中添加MDC中的requestId和userId，推荐使用结构化日志（JSON格式），便于ELK检索。

```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 上下文名称：区分不同服务 -->
    <contextName>order-service</contextName>

    <!-- 1. 控制台输出（JSON格式，包含MDC信息） -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <!-- 包含MDC中的requestId和userId -->
            <includeMdcKeyName>requestId</includeMdcKeyName>
            <includeMdcKeyName>userId</includeMdcKeyName>
            <!-- 自定义固定字段：服务名、环境 -->
            <customFields>"service":"order-service","env":"prod"</customFields>
            <!-- 定义日志字段名，便于解析 -->
            <fieldNames>
                <timestamp>timestamp</timestamp>
                <level>level</level>
                <message>message</message>
                <logger>logger</logger>
                <thread>thread</thread>
                <stack_trace>stackTrace</stack_trace>
            </fieldNames>
        </encoder>
    </appender>

    <!-- 2. 文件输出（滚动策略，避免文件过大） -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/order-service.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/order-service.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory> <!-- 保留30天日志 -->
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <includeMdcKeyName>requestId</includeMdcKeyName>
            <includeMdcKeyName>userId</includeMdcKeyName>
            <customFields>"service":"order-service","env":"prod"</customFields>
        </encoder>
    </appender>

    <!-- 3. 日志级别配置：生产环境INFO -->
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>

    <!-- 自定义包日志级别：mapper层WARN，减少冗余 -->
    <logger name="com.demo.mapper" level="WARN" additivity="false">
        <appender-ref ref="CONSOLE" />
    </logger>
</configuration>
```

### 第四步：业务代码中使用，无需手动拼接链路标识 ###

配置完成后，业务代码中正常输出日志即可，MDC会自动将requestId和userId带入日志，无需手动拼接。

```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {
    private final Logger log = LoggerFactory.getLogger(OrderController.class);

    @PostMapping("/order/create")
    public Result createOrder(@RequestBody OrderCreateDTO dto) {
        log.info("开始创建订单，请求参数：{}", dto); // 自动携带requestId、userId
        try {
            // 调用服务层创建订单
            String orderNo = orderService.createOrder(dto);
            log.info("订单创建成功，订单号：{}", orderNo); // 自动携带链路标识
            return Result.success("创建成功", orderNo);
        } catch (Exception e) {
            log.error("订单创建失败", e); // 异常日志也携带链路标识，便于定位
            return Result.fail("创建失败");
        }
    }
}

// 通用返回结果类（简化代码）
class Result {
    private int code;
    private String message;
    private Object data;

    public static Result success(String message, Object data) {
        return new Result(200, message, data);
    }

    public static Result fail(String message) {
        return new Result(500, message, null);
    }

    // 省略getter/setter、构造方法
}
```

### 测试效果：日志自动携带链路标识 ###

启动项目，调用接口 /order/create，查看日志输出（JSON格式）：

```json
{
  "service": "order-service",
  "env": "prod",
  "requestId": "a1b2c3d4e5f6g7h8i9j0",
  "userId": "1001",
  "timestamp": "2024-05-22T15:30:00.123+08:00",
  "level": "INFO",
  "message": "开始创建订单，请求参数：OrderCreateDTO(productIds=[101,102], amount=299.0)",
  "logger": "com.demo.controller.OrderController",
  "thread": "http-nio-8080-exec-3"
}

{
  "service": "order-service",
  "env": "prod",
  "requestId": "a1b2c3d4e5f6g7h8i9j0",
  "userId": "1001",
  "timestamp": "2024-05-22T15:30:00.234+08:00",
  "level": "INFO",
  "message": "订单创建成功，订单号：ORDER20240522001",
  "logger": "com.demo.controller.OrderController",
  "thread": "http-nio-8080-exec-3"
}
```

通过 `requestId: a1b2c3d4e5f6g7h8i9j0`，可以在海量日志中快速筛选出该次请求的所有链路日志，包括控制器、服务层、DAO层的日志。

## 四、高级用法：线程池/定时任务中的MDC传递 ##

MDC基于ThreadLocal实现，而ThreadLocal的特性是“线程隔离”——当请求进入异步线程（如线程池、定时任务）时，子线程无法继承父线程的MDC上下文，导致链路中断。这是MDC使用中最常见的问题，需通过“手动传递MDC上下文”解决。

### 线程池中的MDC传递：装饰器模式 ###

通过自定义线程池，使用装饰器模式包装Runnable/Callable，在子线程执行前将父线程的MDC上下文传递过去，执行后清除子线程的MDC。

```java
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.*;

/**
 * 支持MDC传递的线程池
 */
public class MdcThreadPoolExecutor extends ThreadPoolExecutor {

    public MdcThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public MdcThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    // 重写execute方法，包装Runnable
    @Override
    public void execute(Runnable command) {
        super.execute(wrap(command, MDC.getCopyOfContextMap()));
    }

    // 重写submit方法，包装Callable
    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(wrap(task, MDC.getCopyOfContextMap()));
    }

    /**
     * 包装Runnable，传递MDC上下文
     */
    private Runnable wrap(Runnable runnable, Map<String, String> context) {
        return () -> {
            try {
                // 子线程执行前，将父线程的MDC上下文设置到当前线程
                MDC.setContextMap(context);
                runnable.run();
            } finally {
                // 子线程执行后，清除MDC，避免线程复用污染
                MDC.clear();
            }
        };
    }

    /**
     * 包装Callable，传递MDC上下文
     */
    private <T> Callable<T> wrap(Callable<T> callable, Map<String, String> context) {
        return () -> {
            try {
                MDC.setContextMap(context);
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }
}
```

使用自定义线程池：

```java
// 配置自定义线程池
@Configuration
public class ThreadPoolConfig {

    @Bean
    public ThreadPoolExecutor mdcThreadPoolExecutor() {
        return new MdcThreadPoolExecutor(
                5, // 核心线程数
                10, // 最大线程数
                60, // 空闲线程存活时间
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100),
                new ThreadFactoryBuilder().setNameFormat("mdc-thread-%d").build()
        );
    }
}

// 业务中使用
@Service
public class OrderService {

    @Autowired
    private ThreadPoolExecutor mdcThreadPoolExecutor;

    public String createOrder(OrderCreateDTO dto) {
        log.info("订单创建开始，进入主线程");
        
        // 异步处理订单通知（子线程会继承MDC上下文）
        mdcThreadPoolExecutor.execute(() -> {
            log.info("异步处理订单通知，productIds：{}", dto.getProductIds()); // 携带requestId
        });
        
        return generateOrderNo();
    }
}
```

### 定时任务中的MDC传递：手动绑定 ###

定时任务没有“父线程”，需在任务执行前手动生成requestId并绑定MDC，执行后清除。

```java
import org.slf4j.MDC;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.UUID;

@Component
public class OrderTimerTask {
    private final Logger log = LoggerFactory.getLogger(OrderTimerTask.class);

    @Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
    public void cleanExpiredOrder() {
        // 手动生成requestId并绑定MDC
        String requestId = UUID.randomUUID().toString().replace("-", "");
        MDC.put("requestId", requestId);
        try {
            log.info("开始清理过期订单");
            // 执行清理逻辑
            orderMapper.cleanExpiredOrder();
            log.info("清理过期订单完成");
        } catch (Exception e) {
            log.error("清理过期订单失败", e);
        } finally {
            // 清除MDC
            MDC.clear();
        }
    }
}
```

## 五、MDC避坑指南：这些错误别踩 ##

- 坑点1：忘记调用MDC.clear() ：导致线程池复用MDC信息，不同请求的日志混淆。 规避：必须在请求结束（拦截器afterCompletion）、子线程执行后（finally块）调用MDC.clear()；

- 坑点2：异步线程未传递MDC：导致子线程日志缺失链路标识，链路中断。 规避：使用支持MDC传递的自定义线程池，或手动在子线程中传递MDC上下文；

- 坑点3：MDC存储敏感信息：将密码、手机号等敏感信息存入MDC，随日志泄露。 规避：MDC仅存储链路标识（requestId、userId），敏感信息需脱敏后再输出；

- 坑点4：分布式服务间未传递requestId：跨服务调用时，requestId未从请求头传递，导致跨服务链路断裂。 规避：网关生成requestId后，通过Feign拦截器将requestId放入请求头，下游服务通过拦截器获取并绑定MDC；

- 坑点5：日志格式未配置MDC字段：MDC已绑定信息，但日志未输出，白做无用功。 规避：务必在logback-spring.xml中配置includeMdcKeyName，确保MDC信息被带入日志。

## 六、总结：MDC的核心价值与适用场景 ##

MDC的核心价值在于“*低成本实现日志链路追踪*”——无需引入复杂的链路追踪框架（如SkyWalking、Pinpoint），仅通过简单配置，就能让日志具备链路串联能力，大幅提升分布式系统问题排查效率。

**适用场景**：

- 分布式微服务架构（如Spring Cloud、Dubbo）；
- 存在多线程异步处理的场景（线程池、定时任务）；
- 需要快速定位全链路日志的业务场景（如支付、订单、交易核心链路）。

最后提醒：MDC是日志优化的“基础工具”，建议结合结构化日志（JSON）、ELK日志分析平台使用，才能发挥最大价值。如果你的项目还在为日志排查烦恼，赶紧落地MDC吧！
