Spring Boot中使用 MDC实现请求TraceId全链路透传

前言

在 Spring Boot 项目中排查问题时,我们通常需要查看日志。但当请求量较大时,很难区分日志属于哪一次请求。为了解决这个问题,我们可以给每个请求设置一个 traceId

使用 traceId 时,可以通过某一条日志获取对应的 traceId,然后快速查询同一请求的所有日志,从而大大提高排查效率。

本文主要介绍如何在项目中使用 Spring 自带的 MDC(Mapped Diagnostic Context) 来实现 traceId,并处理以下场景:

  • HTTP 请求
  • MQ(RabbitMQ)
  • 线程池异步任务

MDC实现原理

MDC 是 SLF4J/Logback 提供的 线程级日志上下文存储 。它内部通过 ThreadLocal<Map<String, String>> 保存上下文信息。

  • 当在某个线程里执行 MDC.put("traceId", "xxx") 时,traceId 会存入当前线程的 ThreadLocal 中。
  • 日志框架在输出日志时,会自动从 MDC 中获取 traceId 并填入日志模板。
  • 不同线程的 MDC 是独立的,每个线程都有自己的上下文,不会互相干扰。

logback文件的处理

需要在logback-spring.xml文件的property标签中日志输出模板中添加traceId的变量占位符,添加内容[traceId:%X{traceId}]

java 复制代码
    <property name="CONSOLE_LOG_PATTERN"
              value="${DEFAULT_CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} [traceId:%X{traceId}] %clr(${LOG_LEVEL_PATTERN:-%5p})
              %clr(${PID:- }){magenta} %clr(---){faint} %clr([%t]){faint} %clr(%-40.40logger{39}){cyan}
              %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

根据自己的需要添加就行,我这里的格式是:[traceId:7e01902b18ef4c2b9f49609c57d769fa]

Http请求处理

只需要添加一个过滤器设置traceId即可

java 复制代码
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.util.UUID;

/**
 * 日志traceId功能
 *
 * @author: Czw
 * @create: 2025-11-06 10:31
 **/
@Component
public class TraceIdFilter implements Filter {
    private static final String TRACE_ID = "traceId";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        try {
            String traceId = UUID.randomUUID().toString().replace("-", "");
            MDC.put(TRACE_ID, traceId);
            chain.doFilter(request, response);
        } finally {
            MDC.remove(TRACE_ID);
        }
    }
}

线程池

需要单独处理Callable和Runnable,在外面包一层。由于线程池中的线程是复用的,所以在用完之后需要在finnally中清除设置的traceId,避免影响下一次任务

java 复制代码
/**
     * 异步执行
     *
     * @param task 任务
     */
    public void execute(Runnable task) {
        defaultThreadPoolExecutor.execute(wrap(task, MDC.getCopyOfContextMap()));
    }

    /**
     * 提交一个有返回值的异步任务
     */
    public <T> Future<T> submit(Callable<T> task) {
        return defaultThreadPoolExecutor.submit(wrap(task, MDC.getCopyOfContextMap()));
    } 

/**
     * 封装 Runnable,复制 MDC 上下文
     */
    private Runnable wrap(Runnable task, Map<String, String> contextMap) {
        return () -> {
            Map<String, String> previous = MDC.getCopyOfContextMap();
            if (contextMap != null) {
                MDC.setContextMap(contextMap);
            } else {
                MDC.clear();
            }
            try {
                task.run();
            } finally {
                // 恢复线程池线程原来的 MDC,避免影响下一次任务
                if (previous != null) {
                    MDC.setContextMap(previous);
                } else {
                    MDC.clear();
                }
            }
        };
    }

    /**
     * 封装 Callable,复制 MDC 上下文
     */
    private <T> Callable<T> wrap(Callable<T> task, Map<String, String> contextMap) {
        return () -> {
            Map<String, String> previous = MDC.getCopyOfContextMap();
            if (contextMap != null) {
                MDC.setContextMap(contextMap);
            } else {
                MDC.clear();
            }
            try {
                return task.call();
            } finally {
                // 恢复线程池线程原来的 MDC,避免影响下一次任务
                if (previous != null) {
                    MDC.setContextMap(previous);
                } else {
                    MDC.clear();
                }
            }
        };
    }

mq(RabbitMq)

MQ的的话需要在sender时统一获取发送时的traceId,然后设置到mq的header中,然后利用Spring AMQP 提供了 RabbitListenerAdvice 机制,可以对所有消费者统一处理,不需要在每一个consumer进行处理

消息生产者处理:

java 复制代码
    /**
     * 同步发送mq (不管消费者有没有消费到,发出去消息就结束)
     *
     * @param typeEnum
     * @param message
     */
    public <T> void sendMq(MqEnum.TypeEnum typeEnum, MqMessage<T> message) {
        rabbitTemplate.convertAndSend(MqEnum.Exchange.EXCHANGE_NAME, typeEnum.getRoutingKey(), message,
                msg -> {
                    String traceId = MDC.get(TRACE_ID);
                    if (traceId == null) {
                        traceId = UUID.randomUUID().toString().replace("-", "");
                        MDC.put(TRACE_ID, traceId);
                    }
                    msg.getMessageProperties().getHeaders().put(TRACE_ID, traceId);
                    return msg;
                });
    }

利用Advice机制获取发送来的traceId然后设置到当前消费者的线程中

java 复制代码
/**
     * 透传MDC
     * sendMq时设置MDC到header中,消费端
     *
     * @return {@link Advice }
     * @author Czw
     * @date 2025/11/06
     */
    @Bean
    public Advice traceIdAdvice() {
        return (MethodInterceptor) invocation -> {
            Object[] args = invocation.getArguments();
            String traceId = null;

            for (Object arg : args) {
                if (arg instanceof Message message) {
                    traceId = (String) message.getMessageProperties().getHeaders().get(TRACE_ID);
                    break;
                }
            }

            if (traceId != null) {
                MDC.put(TRACE_ID, traceId);
            }

            try {
                return invocation.proceed();
            } finally {
                MDC.remove(TRACE_ID);
            }
        };
    }

		/**
     * 设置自定义的traceIdAdvice
     *
     * @param connectionFactory connectionFactory
     * @param traceIdAdvice     traceIdAdvice
     * @return {@link SimpleRabbitListenerContainerFactory }
     * @author Czw
     * @date 2025/11/06
     */
    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
            ConnectionFactory connectionFactory,
            Advice traceIdAdvice) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setAdviceChain(traceIdAdvice);
        return factory;
    }

效果

测试接口

java 复制代码
@GetMapping(value = "/test/traceId/async")
public Result<NullResult> traceId() {
    log.info("主 traceId");
    asyncExecutors.execute(() -> log.info("execute traceId"));

    asyncExecutors.submit(() -> {
        log.info("submit traceId");
        return "ok";
    });

    List<Runnable> list = new ArrayList<>();
    list.add(() -> log.info("execute list traceId"));
    asyncExecutors.execute(list);
    return Result.buildSuccess();
}

@GetMapping(value = "/test/traceId/mq")
public Result<NullResult> mq() {
    log.info("主mq traceId");
    MqMessage<String> message = new MqMessage<>();
    message.setData(JSON.toJSONString(Collections.emptyList()));
    mqSender.sendMq(MqEnum.TypeEnum.PROP_SEND, message);
    return Result.buildSuccess();
}

请求接口时

mq的效果,发送时的traceId与consumer中的traceId一致

线程池的效果,四种线程池的使用时发送方与内部的traceId一致

相关推荐
王元_SmallA2 小时前
pgsql:connection failed connection to server at
java·后端
绝无仅有2 小时前
大厂面试相关文章:深入技术面试中的核心知识点
后端·面试·架构
高山上有一只小老虎2 小时前
购物消费打折
java·算法
绝无仅有2 小时前
面试文章:网络协议与redis安全,https协议,TCP三次握手,四次挥手等面试经典问题
后端·面试·架构
tuokuac3 小时前
@Configuration类中定义的@Bean方法
java
百锦再3 小时前
第8章 模块系统
android·java·开发语言·python·ai·rust·go
没有bug.的程序员3 小时前
Eureka 注册中心原理与服务注册发现机制
java·spring·云原生·eureka·架构·注册中心·服务注册发现
optimistic_chen3 小时前
【Java EE进阶 --- SpringBoot】统一功能处理
java·spring boot·java-ee·json·统一功能处理
IT_陈寒3 小时前
7个鲜为人知的JavaScript性能优化技巧,让你的网页加载速度提升50%
前端·人工智能·后端