1. 基本概念
MDC(Mapped Diagnostic Context)是日志框架(如SLF4J、Logback)提供的一个功能,允许开发者在日志消息中嵌入额外的上下文信息。
这些信息可以在日志处理过程中跨多个日志语句和跨多个线程传递,非常适合用于链路追踪。
2. 设计目标
跨线程传递traceId:确保在一个请求处理过程中,无论涉及到多少个线程或组件,都能通过traceId串联起整个链路。
日志中嵌入traceId:在每个日志语句中嵌入traceId,便于通过日志追踪请求的处理流程。
易于集成和扩展:设计应易于集成到现有的Spring Boot项目中,并便于未来功能的扩展。
3. 架构设计
拦截器:通过实现HandlerInterceptor接口,在请求处理前生成traceId,并将其放入MDC中。在请求处理结束后,从MDC中移除traceId。
线程池配置:对于使用线程池的场景,需要确保子线程能够继承父线程的MDC内容。
日志配置:配置日志框架(如Logback),将MDC中的traceId嵌入到日志格式中。
RabbitMQ:对于异步通信的场景,需要确保MDC内容。
具体实现
1. 添加依赖
在Spring Boot项目的pom.xml中添加相关依赖,包括Spring Boot的启动依赖、SLF4J和Logback等。
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
@Component
public class LogInterceptor implements HandlerInterceptor {
private static final String TRACE_ID = "traceId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String traceId = request.getHeader(TRACE_ID);
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put(TRACE_ID, traceId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
MDC.remove(TRACE_ID);
}
}
3. 配置拦截器
在Spring Boot的配置类中,注册LogInterceptor。
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 WebConfig implements WebMvcConfigurer {
@Autowired
private LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor).addPathPatterns("/**");
}
}
4. 配置Logback
在src/main/resources目录下创建或编辑logback-spring.xml,将MDC中的traceId嵌入到日志格式中。
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{traceId}] - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
5. 线程池考虑
在使用Spring的@Async注解进行异步方法调用时,确保线程之间传递traceId(或其他MDC上下文信息)通常需要一些额外的配置或自定义的线程池实现。
这是因为Spring默认的线程池(如SimpleAsyncTaskExecutor)并不支持InheritableThreadLocal,这是传递线程上下文(如MDC)所必需的。
为了在使用@Async时传递traceId,我们可以配置一个自定义的线程池,该线程池使用支持InheritableThreadLocal的Executor。
以下是一个使用ThreadPoolTaskExecutor作为@Async配置的线程池,并确保traceId能够被传递的示例:
5.1 配置线程池
首先,我们需要配置一个ThreadPoolTaskExecutor,这是Spring提供的线程池实现,支持多种配置,包括核心线程数、最大线程数、队列容量等。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Bean
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("Async-");
// 这一步是关键,确保使用InheritableThreadLocal
executor.setTaskDecorator(runnable -> new MdcTaskDecorator(runnable));
executor.initialize();
return executor;
}
// 自定义的Runnable装饰器,用于设置MDC
private static class MdcTaskDecorator implements Runnable {
private final Runnable delegate;
public MdcTaskDecorator(Runnable delegate) {
this.delegate = delegate;
}
@Override
public void run() {
// 假设我们已经有一个方法可以从ThreadLocal或某个地方获取当前线程的traceId
String traceId = MDC.get("traceId");
if (traceId != null) {
MDC.put("traceId", traceId);
}
try {
delegate.run();
} finally {
// 清理MDC,避免内存泄漏
MDC.clear();
}
}
}
}
注意:上面的MDC.get(“traceId”)和MDC.put(“traceId”, traceId)是假设的调用,因为MDC本身并不直接提供这样的静态方法。
实际上,你需要使用org.slf4j.MDC的getCopyOfContextMap()和setContextMap()(但后者并不直接支持,所以通常我们直接操作InheritableThreadLocal)。
不过,由于InheritableThreadLocal的继承性,通常我们只需要在父线程中设置好MDC,子线程就会自动继承。
这里的MdcTaskDecorator主要是为了确保在某些特殊情况下(如MDC在父线程中未被设置)能够正确地设置MDC。
然而,在大多数情况下,如果你的拦截器已经正确地在每个请求开始时设置了MDC,并且你使用的是支持InheritableThreadLocal的线程池(如上面的ThreadPoolTaskExecutor),那么子线程应该会自动继承父线程的MDC,你不需要在MdcTaskDecorator中显式设置它。
5.2 使用@Async
现在,你可以在你的服务或组件中使用@Async注解来标记异步方法了。
只要确保这些异步方法是由上面配置的线程池执行的,那么它们就应该能够继承父线程的MDC上下文(包括traceId)。
@Service
public class AsyncService {
@Async
public void executeAsyncTask() {
// 这个方法会在自定义的线程池中执行,并且应该能够访问到父线程的MDC上下文
String traceId = MDC.get("traceId"); // 假设有这样一个静态方法或类似机制来获取MDC中的traceId
log.info("Executing async task with traceId: {}", traceId);
}
}
6. RabbitMQ考虑
在异步消息队列(MQ)中传递traceId(或其他跟踪标识符)是分布式追踪系统中的一个常见需求,它有助于跨多个服务和组件跟踪请求的执行路径。
以下是几种在异步MQ中传递traceId的方法:
定义消息发送时的封装方法:在发送消息到RabbitMQ时,确保将traceId添加到消息的头部。
定义消息接收时的处理逻辑:在接收消息时,从头部中提取traceId,并将其放入适当的上下文中以供后续处理使用。
发送消息时封装traceId
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import java.util.HashMap;
import java.util.Map;
public void sendMessageWithTraceId(String queueName, byte[] messageBody, String traceId) throws Exception {
Channel channel = // 获取RabbitMQ的Channel连接
Map<String, Object> headers = new HashMap<>();
headers.put("x-trace-id", traceId);
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.headers(headers)
.build();
channel.basicPublish("", queueName, props, messageBody);
}
接收消息时处理traceId
对于接收端,你需要从消息的头部中提取traceId,并将其放入一个合适的上下文中。但是,由于MDC的局限性,你可能需要使用一个自定义的上下文管理器。
import com.rabbitmq.client.*;
public void receiveMessage(String queueName) throws Exception {
ConnectionFactory factory = // 创建RabbitMQ的连接工厂
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(queueName, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
Map<String, Object> headers = delivery.getProperties().getHeaders();
String traceId = (String) headers.get("x-trace-id");
// 将traceId放入自定义的上下文中
// 例如:自定义的ContextManager.setTraceId(traceId);
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
// 处理消息...
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
到此整个设计完毕。