Spring Boot实现日志链路追踪

部署运行你感兴趣的模型镜像


目的:有时候一个业务调用链的场景很长,其中调了各种各样的方法,现在需要把同一次业务调用链上的日志串起来,也就是给同一个 trace ID 即可。


分析及实现过程:

什么是MDC

1、MDC(映射诊断上下文)是一个把键值对(Map<String,String>)绑定到当前线程上的机制。主要用于把请求级别或线程级别的上下文信息(如 traceId、userId等)自动带到日志输出中,便于日志追踪与排查。它是 SLF4J 提供的 API

2、MDC 在大多数实现里基于 ThreadLocal<Map<String,String>>。也就是说,MDC 的数据绑定到 线程 上,只有当前线程能看到自己的 MDC

3、大多数 Spring Boot 项目默认已经包含:spring-boot-starter-logging(内部包含 Logback 和 slf4j),因此通常无需额外引依赖。

使用:MDC.put("TRACE_ID", "xxx"),则在日志格式里用 %X{TRACE_ID}(Logback)或 ${ctx:TRACE_ID}(Log4j2)取值。


常用API

常见方法(都来自 org.slf4j.MDC):

  • MDC.put(String key, String value):设置键值对到当前线程 MDC。
  • MDC.get(String key):获取当前线程中 key 对应的值。
  • MDC.remove(String key):移除某个 key。
  • MDC.clear():清空当前线程全部 MDC。
  • MDC.getCopyOfContextMap():获取当前线程 MDC 的 拷贝(返回 Map<String,String>null)。用于把父线程上下文传给子线程。
  • MDC.setContextMap(Map<String,String>):把给定 map 设为当前线程的 MDC(覆盖)。

注意:尽量使用 getCopyOfContextMap() 获取拷贝,避免直接传递可变引用导致并发修改问题


代码实现

可以参考上传到 Gitee 中的代码:完整代码
下面贴出主要代码,并附带分析:

1、再来看看主要的实现代码

1.1 先自定义线程池,并配置线程池

/**
 * 自定义ThreadPoolTaskExecutor线程池
 */
public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

    // 重写父类的方法,进行任务包装:为了将父线程的MDC传进去线程池
    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}
/**
 * 线程池配置
 */
@Configuration
public class MyThreadPoolTaskExecutorConfig {
    @Bean("MyExecutor")
    public Executor asyncExecutor() {
        MyThreadPoolTaskExecutor myExecutor = new MyThreadPoolTaskExecutor();
        myExecutor.setCorePoolSize(5);
        myExecutor.setMaxPoolSize(5);
        myExecutor.setQueueCapacity(500);
        myExecutor.setKeepAliveSeconds(60);
        myExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        myExecutor.setThreadNamePrefix("ZFP");
        myExecutor.initialize();
        return myExecutor;
    }
}

1.2 生成并使用 traceId

public final class ThreadMdcUtil {
    private static final String TRACE_ID = "TRACE_ID";

    // 生成唯一标识
    public static String generateTraceId() {
        return UUID.randomUUID().toString();
    }

    // 假如当前线程没有traceId,就生成一个并设置
    public static void setTraceIdIfAbsent() {
        if (MDC.get(TRACE_ID) == null) {
            MDC.put(TRACE_ID, generateTraceId());
        }
    }

    /**
     * Callable(异步任务有返回值):父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
     */
    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            // 如果父线程传进来的(他的MDC)没有值
            if (context == null) {
                // 清理子线程的MDC,为下一步做准备
                MDC.clear();
            } else {
                // 有值:父线程的MDC复制给子线程
                MDC.setContextMap(context);
            }

            // context!=null,并且context里面有traceId,
            // 上面else不是做了,这里为什么还做,为了确保有TRACE_ID,兜底
            setTraceIdIfAbsent();

            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }

    /**
     * Runnable(异步任务没有返回值):父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
     */
    public static <T> Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            
            setTraceIdIfAbsent();

            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

分析代码:

① 父线程 和 子线程

  • 父线程(调用方):当前正在运行的线程,比如处理HTTP请求的线程、主线程。它决定向线程池提交任务。
  • 子线程(执行方):真正在线程池中执行任务的线程。它不是新建的,而是线程池里复用的工作线程。

父线程是“派任务的人”,子线程是“干活的人”。父线程要告诉子线程:“你干活时要带上我的上下文(trace id、userId 等)”

② 为什么要重写线程池中的提交和执行方法?

如果不重新自定义线程池,子线程看不到父线程的MDC(也就是父线程的MDC怎么传给子线程),比如下图:

在这里插入图片描述

图中:执行 /insert 接口,流程:主线程在拦截器中设置了 MDC ----> 遇到 @Async 注解的方法,由于没有指定线程池,所以使用默认的线程池执行 ----> 子线程肯定拿不到父线程的 MDC,所以主线程有MDC,子线程为空。

所以要重写线程池中的方法,以便将父线程的 MDC 传给子线程。那怎么传?看 ③ 、④

③ 为什么要写这个 wrap 方法?

  • MDC是基于ThreadLocal的 ----> 所以父线程和子线程各自有独立的 MDC 存储空间 ----> 所以子线程默认 拿不到父线程的 MDC 内容
  • 而且如果线程池复用线程,还可能“串号”(也就是:子线程可能残留上一次任务的 MDC)

④ 那怎么理解 wrap 方法?

  • 父线程调用 wrap 方法(创建包装);子线程执行包装内部的代码;目的是 把父线程的 MDC 上下文(比如 traceId)传递给子线程使用

  • 代码解释:

    1. 如果父线程没传任何上下文(context == null),那清空子线程的 MDC(为下一步做准备)----> 否则,把父线程的上下文设置到子线程;

      不管哪种情况,最后都执行一次 setTraceIdIfAbsent()

    2. 当父线程 context 不为 nullMDC.setContextMap(context); 那为什么还需要setTraceIdIfAbsent(); 已经重复了 ,为什么?

      • 如果 context 已经包含了 "TRACE_ID",那 MDC.setContextMap(context) 后,MDC 里肯定已经有这个 key。 这时再调用 setTraceIdIfAbsent(),它检查到 MDC.get("TRACE_ID") != null,就不会再生成新的 traceId。也就是说它不会“重复设置”,但会进行一次判断。
      • 兜底检查:
        • 假如父线程 context 内容为:{ "USER_ID": "u001" }(父线程没 TRACE_ID),子线程 setTraceIdIfAbsent() 会生成一个新的 traceId(兜底);
        • 假如无任何 context(父线程没传 MDC),MDC.clear() 后执行 setTraceIdIfAbsent(),生成一个新的 traceId

      总结:setTraceIdIfAbsent() 的存在是为了“防御”那些没有带 traceId 的任务场景,保证所有线程都有唯一标识。(虽然在拦截器中设置了,但防止意外)

5、运行验证:

① 正常没有异步任务的接口:/pay

在这里插入图片描述

② 有异步任务的接口:/insert

在这里插入图片描述

本来是正常的结果,但是有个问题:就是正常实现了,而且我没指定线程池,它自己就使用了我的线程池,按道理来说,应该使用默认的线程池(SimpleAsyncTaskExecutor,然后实现不了)


再谈@Async

1、当 Spring 处理 @Async 时:如果容器里存在一个(或可识别的)Executor/TaskExecutor bean,Spring 会把它当作默认执行器来使用。所以你“没显式在 @Async 指定线程池”也会用到你定义的线程池 —— 因为 Spring 自动选了容器里那个合适的 Executor 作为默认。

2、@Async 的执行器来源优先级:

  • 如果 @EnableAsync 的配置类实现了 AsyncConfigurer,则 getAsyncExecutor() 返回的 Executor 会被当作默认 executor。否则,若容器中有唯一的 Executor / TaskExecutor 类型的 bean,Spring 会把它当默认 executor。
  • 若容器中有多个 Executor,但有一个名为 taskExecutor(或有 @Primary)的 bean,则会优先使用它。
  • 最后兜底:如果没有找到可用的 executor,Spring 会创建一个 SimpleAsyncTaskExecutor 作为默认(但这通常不是你遇到的情况)。

3、定位是哪一个 Executor

@Component
public class ExecutorInspector {

    @Autowired
    private ApplicationContext ctx;

    @PostConstruct
    public void inspect() {
        Map<String, Executor> execs = ctx.getBeansOfType(Executor.class);
        System.out.println("===== Executors in context =====");
        execs.forEach((name, exe) -> {
            System.out.println("beanName=" + name + ", class=" + exe.getClass().getName());
        });
        System.out.println("================================");
    }
}

4、怎么灵活选择:

@Configuration
public class MyThreadPoolTaskExecutorConfig implements AsyncConfigurer { // 实现这个接口

    @Bean("MyExecutor")
    public Executor asyncExecutor() {
        MyThreadPoolTaskExecutor myExecutor = new MyThreadPoolTaskExecutor();
        myExecutor.setCorePoolSize(5);
        myExecutor.setMaxPoolSize(5);
        myExecutor.setQueueCapacity(500);
        myExecutor.setKeepAliveSeconds(60);
        myExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        myExecutor.setThreadNamePrefix("ZFP");
        myExecutor.initialize();
        return myExecutor;
    }


    // 默认的执行器
    @Override
    public Executor getAsyncExecutor() {
        return new SimpleAsyncTaskExecutor();
    }
}

结束,记得点赞收藏哦!!!

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

### 回答1: Spring Cloud 可以通过使用 Sleuth 库来实现链路追踪。 Sleuth 是一个用于在分布式系统中跟踪请求的库,它会在请求中注入一些信息,如请求 ID、调用链等,这些信息可以在整个请求的生命周期中传递。 首先,需要在 Spring Boot 应用中添加 Sleuth 的依赖: ``` <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> ``` 然后,需要配置日志系统,以便 Sleuth 能够记录请求信息。比如,可以使用 Logback 和 SLF4J 进行配置: ``` <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </dependency> ``` 最后,可以使用各种工具(如 Zipkin、Elastic Stack 等)来收集和展示请求信息。 这样,就可以在分布式系统中实现链路追踪了。 ### 回答2: Spring Cloud提供了一种称为"Sleuth"的组件,它可以实现链路追踪。链路追踪的目的是可视化和监控微服务架构中的请求流。下面是Spring Cloud Sleuth如何实现链路追踪的步骤: 1. 引入依赖:在项目的pom.xml文件中,引入Spring Cloud Sleuth的依赖。例如: ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> ``` 2. 配置跟踪信息:在微服务的配置文件(例如application.yml)中,配置服务的名称(spring.application.name)和端口(server.port)。例如: ```yaml spring: application: name: your-service-name server: port: 8080 ``` 3. 启用链路追踪:在微服务的主类上,使用`@EnableSleuth`注解来启用Spring Cloud Sleuth的链路追踪功能。例如: ```java @SpringBootApplication @EnableSleuth public class YourServiceApplication { public static void main(String[] args) { SpringApplication.run(YourServiceApplication.class, args); } } ``` 4. 发起请求:通过HTTP或其他方式在微服务之间发起请求。每个请求都会包含一个唯一的跟踪ID。 5. 查看链路追踪信息:在日志中,可以看到每个请求的跟踪ID和相应的父跟踪ID。这些信息可以帮助我们追踪和分析请求的流动。 通过使用Spring Cloud Sleuth,我们可以轻松地实现在微服务架构中进行链路追踪。这对于追踪和分析请求流是非常有价值的,可以提高微服务系统的性能和可维护性。 ### 回答3: Spring Cloud提供了一种称为Sleuth的链路追踪解决方案,可以帮助我们追踪分布式系统中的请求流程。下面是Spring Cloud Sleuth实现链路追踪的步骤: 1. 配置依赖:在项目的pom.xml文件中,添加Spring Cloud Sleuth的依赖: ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> ``` 2. 配置日志:在系统的主配置文件中,配置日志相关的属性,例如日志输出格式、日志级别等。 3. 创建并配置Zipkin服务器:Zipkin是一个用于收集、存储和展示追踪数据的服务器。我们可以使用Docker或者下载其JAR包来启动Zipkin服务器。 4. 配置请求追踪:在项目的主类上添加@EnableZipkinServer注解,启用Zipkin追踪。 5. 添加日志跟踪:在程序的关键组件中(例如Controller层、Service层)添加相关注解,Spring Cloud Sleuth会自动为每个请求生成一个唯一的请求ID,并将该ID添加到日志中。 这样,当有请求进入系统时,系统就会自动为该请求生成一个唯一的ID,然后在请求经过不同的模块和服务时,这个ID会在日志中进行传递和记录。通过查看日志信息,我们可以很方便地追踪请求在系统中的流向和调用。 总之,通过使用Spring Cloud Sleuth,我们可以很容易地实现链路追踪功能,帮助我们分析和排查分布式系统中的问题,并提高系统的可用性和稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小学鸡!

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值