Java多線程之間日志traceId傳遞方式
Java多線程之間日志traceId傳遞
在生產(chǎn)環(huán)境中,由于處在并發(fā)環(huán)境,所以日志輸出的順序散落在各個不同行,通過traceId就能夠快速定位到同一個請求的多個不同的日志輸出,可以很方便地跟蹤請求并定位問題。
但是,如果在代碼中使用了多線程,那么就會發(fā)現(xiàn),新開的線程不會攜帶父線程traceId。于是,通過繼承父線程的MDC上下文信息,使得新開的線程與父線程保持一致的traceId。
MDC說明
MDC(Mapped Diagnostic Context)是一種常用的日志記錄技術(shù),MDC可以將關(guān)鍵信息存儲在線程上下文中,并在需要時將其傳遞到調(diào)用鏈的不同組件中。
使用MDC傳遞日志的好處:
- 方便跟蹤請求:通過 MDC,可以在整個請求生命周期中記錄和傳遞關(guān)鍵信息,例如請求 ID、用戶 ID 等,這樣可以方便地跟蹤請求并定位問題。
- 提高調(diào)試效率:MDC 可以存儲調(diào)用鏈中各個組件的上下文信息,從而使得在調(diào)試時可以更快速地診斷問題,縮短故障排除時間。
- 支持分布式系統(tǒng):在分布式系統(tǒng)中,MDC 可以在不同節(jié)點之間傳遞關(guān)鍵信息,使得在跨節(jié)點調(diào)用時可以快速定位問題。
- 提高代碼可讀性:MDC 記錄的上下文信息可以被日志輸出格式化為易于閱讀的形式,提升代碼可讀性。
實現(xiàn)代碼
/** ?* 繼承ThreadPoolTaskExecutor,實現(xiàn)多線程處理任務時傳遞日志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上下文信息,避免造成內(nèi)存泄漏 ? ? ? ? ? ? ? ? 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上下文信息,避免造成內(nèi)存泄漏 ? ? ? ? ? ? ? ? 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-"); ? ? // 拒絕策略為調(diào)用者執(zhí)行 ? ? executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); ? ? executor.initialize(); ? ? return executor; }
線程間傳遞Traceid問題
作為一個程序員,在工作當中排查問題是很常見的,但在多線程的情況下,想通過日志跟蹤問題,對于初學者是有點困難的。
在這里分享下如何快速定位多線程環(huán)境下的調(diào)用鏈路,方便調(diào)用日志的查看以及問題的定位
方案
在日志打印時增加Traceid, 方便整個調(diào)用鏈路的追蹤
- 同步調(diào)用: 能根據(jù)日志打印的Traceid追蹤到整條調(diào)用鏈路
- 異步調(diào)用: 如果不做其他處理異步調(diào)用的程序打印的日志會丟失Traceid,也就沒法通過這個Traceid查看調(diào)用鏈路。
這時我們就需要對異步調(diào)用的程序進行處理,使得異步調(diào)用時日志文件也能輸出Traceid,并通過Traceid查看調(diào)用鏈路
實現(xiàn)
異步調(diào)用的開啟方式大致可為2種,
1、 new Thread()
2、線程池技術(shù)
在這里我們講的是利用線程池執(zhí)行異步操作,所以我們需要對線程池進行改造,使得其能傳遞Traceid,并在后續(xù)的程序執(zhí)行打印日志時能輸出Traceid
我們知道異步調(diào)用主要的方式有: 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 我們還需要進行其他的處理,
需要讓程序需要用到封裝之后的方法,不然之前做的都是無用功,那么我們需要如何處理呢?
上面提到要利用線程池,但是我們?nèi)绾巫尵€程池使用改造之后的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 ,重寫線程執(zhí)行的方法。
到這我們就做完了大部分的準備工作,還剩下最關(guān)鍵的就是讓程序用到我們封裝后的線程池。
我們可以在聲明線程池的時候,直接使用我們封裝好的線程池(因為繼承了ThreadPoolTaskExecutor)
@Bean ? ? public ThreadPoolTaskExecutor taskExecutor() { ? ? ? ? ThreadPoolTaskExecutor taskExecutor = new ThreadPoolMdcWrapper(); ? ? ? ? //核心線程數(shù),默認為1 ? ? ? ? taskExecutor.setCorePoolSize(1); ? ? ? ? //最大線程數(shù),默認為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; ? ? }
到這我們所做的準備工作,改造工作也就結(jié)束了,剩下的就是使用了。只要在程序異步調(diào)用時,利用聲明好的taskExecutor線程池進行調(diào)用,就可以在線程上下文正確傳遞Traceid了。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
java使用Runtime執(zhí)行系統(tǒng)命令遇到的問題
這篇文章主要介紹了java使用Runtime執(zhí)行系統(tǒng)命令遇到的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11Spring Data JPA 建立表的聯(lián)合主鍵
這篇文章主要介紹了Spring Data JPA 建立表的聯(lián)合主鍵。本文詳細的介紹了2種方式,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-04-04使用Post方式提交數(shù)據(jù)到Tomcat服務器的方法
這篇將介紹使用Post方式提交數(shù)據(jù)到服務器,由于Post的方式和Get方式創(chuàng)建Web工程是一模一樣的,只用幾個地方的代碼不同,這篇文章主要介紹了使用Post方式提交數(shù)據(jù)到Tomcat服務器的方法,感興趣的朋友一起學習吧2016-04-04SpringCloud之Zuul網(wǎng)關(guān)原理及其配置講解
這篇文章主要介紹了SpringCloud之Zuul網(wǎng)關(guān)原理及其配置講解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03