SpringBoot结合MDC实现链路追踪设计

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等。

org.springframework.boot spring-boot-starter-web org.slf4j slf4j-api 1.7.30 **2. 创建拦截器** 创建一个拦截器LogInterceptor,用于在请求处理前生成traceId并放入MDC,在请求处理结束后从MDC中移除traceId。

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 -> { });  

}

到此整个设计完毕。

  • 11
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愚公搬程序

你的鼓励将是我们最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值