前言
在 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 提供了 RabbitListener 的 Advice 机制,可以对所有消费者统一处理,不需要在每一个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一致
