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}]
<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即可
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";@Overridepublic 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,避免影响下一次任务
/*** 异步执行** @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 提供了 RabbitListener 的 Advice 机制,可以对所有消费者统一处理,不需要在每一个consumer进行处理
消息生产者处理:
/*** 同步发送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然后设置到当前消费者的线程中
/*** 透传MDC* sendMq时设置MDC到header中,消费端** @return {@link Advice }* @author Czw* @date 2025/11/06*/@Beanpublic 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*/@Beanpublic SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory,Advice traceIdAdvice) {SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();factory.setConnectionFactory(connectionFactory);factory.setAdviceChain(traceIdAdvice);return factory;}
效果
测试接口
@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一致

