Java多线程之间日志traceId传递方式

 更新时间:2023年08月28日 08:42:25   作者:丶只有影子  
这篇文章主要介绍了Java多线程之间日志traceId传递方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

Java多线程之间日志traceId传递

在生产环境中,由于处在并发环境,所以日志输出的顺序散落在各个不同行,通过traceId就能够快速定位到同一个请求的多个不同的日志输出,可以很方便地跟踪请求并定位问题。

但是,如果在代码中使用了多线程,那么就会发现,新开的线程不会携带父线程traceId。于是,通过继承父线程的MDC上下文信息,使得新开的线程与父线程保持一致的traceId。

MDC说明

MDC(Mapped Diagnostic Context)是一种常用的日志记录技术,MDC可以将关键信息存储在线程上下文中,并在需要时将其传递到调用链的不同组件中。

使用MDC传递日志的好处:

  • 方便跟踪请求:通过 MDC,可以在整个请求生命周期中记录和传递关键信息,例如请求 ID、用户 ID 等,这样可以方便地跟踪请求并定位问题。
  • 提高调试效率:MDC 可以存储调用链中各个组件的上下文信息,从而使得在调试时可以更快速地诊断问题,缩短故障排除时间。
  • 支持分布式系统:在分布式系统中,MDC 可以在不同节点之间传递关键信息,使得在跨节点调用时可以快速定位问题。
  • 提高代码可读性:MDC 记录的上下文信息可以被日志输出格式化为易于阅读的形式,提升代码可读性。

实现代码

/**
 * 继承ThreadPoolTaskExecutor,实现多线程处理任务时传递日志traceId
 */
public class ThreadPoolTaskExecutorMdcUtil extends ThreadPoolTaskExecutor {
    @Override
    public void execute(Runnable task) {
        super.execute(wrap(task));
    }
    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(wrap(task));
    }
    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(wrap(task));
    }
    private <T> Callable<T> wrap(final Callable<T> callable) {
        // 获取当前线程的MDC上下文信息
        Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            if (context != null) {
                // 传递给子线程
                MDC.setContextMap(context);
            }
            try {
                return callable.call();
            } finally {
                // 清除MDC上下文信息,避免造成内存泄漏
                MDC.clear();
            }
        };
    }
    private Runnable wrap(final Runnable runnable) {
        Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            if (context != null) {
                MDC.setContextMap(context);
            }
            try {
                runnable.run();
            } finally {
                // 清除MDC上下文信息,避免造成内存泄漏
                MDC.clear();
            }
        };
    }
}

之后只要像正常的使用线程池一样使用ThreadPoolTaskExecutorMdcUtil类即可。

例如,注入一个线程池Bean代码示例:

@Bean("thread-pool-receive")
public ThreadPoolTaskExecutor receiveThreadPoolExecutor() {
    // new的是自定义的线程池
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutorMdcUtil();
    executor.setCorePoolSize(1);
    executor.setMaxPoolSize(10);
    // 缓存队列
    executor.setQueueCapacity(10000);
    // 允许线程的空闲时间60秒:
    executor.setKeepAliveSeconds(60);
    // 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
    executor.setThreadNamePrefix("test-");
    // 拒绝策略为调用者执行
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.initialize();
    return executor;
}

线程间传递Traceid问题

作为一个程序员,在工作当中排查问题是很常见的,但在多线程的情况下,想通过日志跟踪问题,对于初学者是有点困难的。

在这里分享下如何快速定位多线程环境下的调用链路,方便调用日志的查看以及问题的定位

方案

在日志打印时增加Traceid, 方便整个调用链路的追踪

  • 同步调用: 能根据日志打印的Traceid追踪到整条调用链路
  • 异步调用: 如果不做其他处理异步调用的程序打印的日志会丢失Traceid,也就没法通过这个Traceid查看调用链路。

这时我们就需要对异步调用的程序进行处理,使得异步调用时日志文件也能输出Traceid,并通过Traceid查看调用链路

实现

异步调用的开启方式大致可为2种,

1、 new Thread()

2、线程池技术

在这里我们讲的是利用线程池执行异步操作,所以我们需要对线程池进行改造,使得其能传递Traceid,并在后续的程序执行打印日志时能输出Traceid

我们知道异步调用主要的方式有: Callable, Runnable

不错,到这里我们要做的就是对Callable, Runnable等方法进行封装,使得其能正确的帮我们传递Traceid

传递Traceid利用都了日志框架中的MDC工具

我们先定义一个工具类,用于生成Traceid

public class ThreadMdcUtil {
    public static String createTraceId() {
        String uuid = UUID.randomUUID().toString();
        return DigestUtils.md5Hex(uuid).substring(8, 24);
    }
    public static void setTraceIdIfAbsent() {
        if (MDC.get(CommonConstant.LOG_TRACE_ID) == null) {
            MDC.put(CommonConstant.LOG_TRACE_ID, createTraceId());
        }
    }
    public static String getTraceId() {
        return MDC.get(CommonConstant.LOG_TRACE_ID);
    }
    public static void setTraceId() {
        MDC.put(CommonConstant.LOG_TRACE_ID, createTraceId());
    }
    public static void setTraceId(String traceId) {
        MDC.put(CommonConstant.LOG_TRACE_ID, traceId);
    }
    public static void clear() {
        MDC.clear();
    }
}

有了Traceid,接下来要做的就是对线程里面的2个主要的方法进行改造,

改造方案如下:

public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }
    public static 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();
            }
        };
    }

对线程的2个主要的方法进行改造之后,我们要使得程序日志正确打印传递的Traceid 我们还需要进行其他的处理,

需要让程序需要用到封装之后的方法,不然之前做的都是无用功,那么我们需要如何处理呢?

上面提到要利用线程池,但是我们如何让线程池使用改造之后的2个方法呢?

在这我们要做的就是对线程池进行封装处理,重写线程池的方法,让其用到我们处理后的线程方法。

public class ThreadPoolMdcWrapper extends ThreadPoolTaskExecutor {
    public ThreadPoolMdcWrapper() {
    }
    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
    @Override
    public void execute(Runnable task, long startTimeout) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), startTimeout);
    }
    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
    @Override
    public ListenableFuture<?> submitListenable(Runnable task) {
        return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
    @Override
    public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
        return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}

继承ThreadPoolTaskExecutor ,重写线程执行的方法。

到这我们就做完了大部分的准备工作,还剩下最关键的就是让程序用到我们封装后的线程池。

我们可以在声明线程池的时候,直接使用我们封装好的线程池(因为继承了ThreadPoolTaskExecutor)

@Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolMdcWrapper();
        //核心线程数,默认为1
        taskExecutor.setCorePoolSize(1);
        //最大线程数,默认为Integer.MAX_VALUE
        taskExecutor.setMaxPoolSize(200);
        //队列最大长度,一般需要设置值>=notifyScheduledMainExecutor.maxNum;默认为Integer.MAX_VALUE
        taskExecutor.setQueueCapacity(2000);
        //线程池维护线程所允许的空闲时间,默认为60s
        taskExecutor.setKeepAliveSeconds(60);
        //线程池对拒绝任务(无线程可用)的处理策略
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 初始化线程池
        taskExecutor.initialize();
        return  taskExecutor;
    }

到这我们所做的准备工作,改造工作也就结束了,剩下的就是使用了。只要在程序异步调用时,利用声明好的taskExecutor线程池进行调用,就可以在线程上下文正确传递Traceid了。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java8的EnumMap源码分析

    Java8的EnumMap源码分析

    这篇文章主要介绍了Java8的EnumMap源码分析,EnumMap 是一个用于存储 key 为枚举类型的 map,底层使用数组实现(K,V 双数组),与其他类型 map 不同的是 EnumMap 底层使用双数组来存储 key 与 value,key 数组会在构造函数中根据 keyType 进行初始化,需要的朋友可以参考下
    2023-11-11
  • 2023年IDEA最新永久激活教程(亲测可用)

    2023年IDEA最新永久激活教程(亲测可用)

    打开电脑,发现 IDEA 又更新了一个小版本,2022.3.2 版本来了,真的是非常高兴,那么这么新的版本怎么激活使用呢?下面小编给大家带来了idea2023年最新永久激活方法,感兴趣的朋友一起看看吧
    2023-04-04
  • Java特性 Lambda 表达式和函数式接口

    Java特性 Lambda 表达式和函数式接口

    这篇文章主要介绍了Java特性 Lambda 表达式和函数式接口,Lambda表达式基于函数式编程思想,也可以称为闭包,是Java 8引入的重要新特性, Lambda允许把函数作为一个方法的参数
    2022-06-06
  • Java可重入锁reentrantLock解析

    Java可重入锁reentrantLock解析

    这篇文章主要介绍了Java可重入锁reentrantLock解析,reentrantLock跟synchronized代码结构差不多,只是多了一个lock和unlock的过程,需要的朋友可以参考下
    2023-12-12
  • 排序算法图解之Java归并排序的实现

    排序算法图解之Java归并排序的实现

    归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。本文主要介绍了归并排序的实现,需要的可以参考一下
    2022-11-11
  • Java11 中基于嵌套关系的访问控制优化问题

    Java11 中基于嵌套关系的访问控制优化问题

    在 Java 语言中,类和接口可以相互嵌套,这种组合之间可以不受限制的彼此访问,包括访问彼此的构造函数、字段、方法,接下来通过本文给大家介绍Java11中基于嵌套关系的访问控制优化问题,感兴趣的朋友一起看看吧
    2022-01-01
  • mybatis-plus中lambdaQuery()与lambdaUpdate()比较常见的使用方法总结

    mybatis-plus中lambdaQuery()与lambdaUpdate()比较常见的使用方法总结

    mybatis-plus是在mybatis的基础上做增强不做改变,简化了CRUD操作,下面这篇文章主要给大家介绍了关于mybatis-plus中lambdaQuery()与lambdaUpdate()比较常见的使用方法,需要的朋友可以参考下
    2022-09-09
  • java实现快速打字游戏

    java实现快速打字游戏

    这篇文章主要为大家详细介绍了java实现快速打字游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-07-07
  • 基于Mybatis Plus实现代码生成器CodeGenerator

    基于Mybatis Plus实现代码生成器CodeGenerator

    这篇文章主要介绍了基于Mybatis Plus实现代码生成器CodeGenerator,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-08-08
  • java网络编程之socket网络编程示例(服务器端/客户端)

    java网络编程之socket网络编程示例(服务器端/客户端)

    这篇文章主要介绍了java socket网络编程的示例,分为服务器端和客户端,大家参考使用吧
    2014-01-01

最新评论