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了。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。