MDC JAVA多線程下日志管理實踐


一、了解MDC
MDC是什么
  MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日志的功能,也可以說是一種輕量級的日志跟蹤工具。
MDC能做什么
  那么通過MDC的概念,我們可以知道,MDC是應用內的線程級別,不是分布式的應用層級別,所以僅靠它無法做到分布式應用調用鏈路跟蹤的需求。它要解決的問題主要是讓我們可以在海量日志數據中快速撈到可用的日志信息。
場景分析
  既然我們知道MDC可以讓我們快速的撈到可用的日志信息,那具體怎么撈呢?我們先來看這樣的一個場景:很多時候,我們一個程序調用鏈可能會很復雜,並且在調用鏈的各個環節中,會對一些關鍵的操作做日志埋點,比如說入參出參、復雜計算后的結果等等信息,但在線上環境是很多用戶使用我們功能的,比如說A程序,每個用戶都在使用了A程序后,打印了A程序方法調用鏈內的所以日志,那我怎么就知道這一堆相同日志中,哪些是同一次請求所打印的呢?可能大家會說:可以看它的線程名啊,HTTP在同一請求中會用同一個線程。一定程度上看線程是可以的,但我們也知道,web服務器不可能無限創建線程的,它內部有個線程池,用於HTTP線程的創建、回收等管理,如果該程序使用頻率是很高,那完全有可能短時間內的幾次請求用的都是同一個線程,這樣的話就解決不了上述所說的:“把一次請求中調用鏈內的所以日志找出來”的需求了。
解決方案
  針對以上的場景,我們可以在一次請求進來的時候,創建一個全局唯一的標識符,該標識符可以沒有業務含義,我們就叫它做“traceId”吧,因為這僅僅只是為了區分每次請求打印了什么信息,接下來,我們知道ThreadLocal這個類是可以共享線程內的數據的,所以我們就可以利用它來實現這個需求了。把traceId放入到ThreadLocal中,然后在我們程序調用鏈中輸出日志時,就可以帶上這個traceId了,比如以下代碼:
String traceId = threadLocal.get();
log.info("這里是打印信息{}", traceId);
  以上方法雖然解決了我們的問題,但是我們每次打印日志都要自己拿一下traceId,這無形增加了我們的工作量和降低了代碼的美觀度,所以我們肯定得想辦法封裝這部分重復的代碼了。而這個封裝的事情MDC就幫我們做了,我們只管在請求最開始時,生成一個traceId,然后放到MDC中就可以了,之后的事情就是按照我們原來的方式打印日志,不用新增其他額外的重復代碼,這個traceId也一直跟隨這個線程的執行完所有的任務。

二、具體實現
實現前效果

  我們先來看看實現MDC前的日志打印效果。我們以logback為例,在配置文件中,定義的日志格式為:
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %thread | [%X{X-B3-TraceId}] | %-5level %logger{50} %msg%n</pattern>
代碼我們打印的日志要包含以下信息:日志打印的時間、線程、TraceId、級別、哪個類打印的和具體打印信息嗎,其中%X{X-B3-TraceId}就是我們接下來要講的內容。跑一下應用看看當前的日志長啥樣:

通過上述打印的日志可以看出問題了,兩次請求,剛好分配了同一個線程http-nio-9191-exec-1處理,這樣我們在海量日志數據的情況下,就很難區分每次請求分別打印了哪一些日志了。

實現后效果

  在上面演示中,我們看到輸出的日志中,有個“[ ]”的字符串,這一塊信息是我們在定義日志格式中的“[%X{X-B3-TraceId}]”,%X{ }是取值的意思,告訴日志框架,需要去MDC獲取key為X-B3-TraceId的值,很明顯,我們並沒有給MDC設置一個key為X-B3-TraceId的值,所以當我們打印日志的時候,這一塊就打印成空字符串了。下面我們來看看給MDC設置了值之后的效果。

創建並設置traceId
我們以SpringMVC為例,在請求最開始時創建traceId,並把該traceId放到MDC中:這一步我們可以使用SpringMVC的攔截器或者AOP來實現。而這里的例子我就使用AOP來實現。

 

需要操作MDC很簡單,使用的工具類就叫做MDC,它是slf4j提供的日志標准包下的一個類,log4j和logback都有實現,然后往MDC設置一個key為X-B3-TraceId的值,X-B3-TraceId就是我們上述日志格式定義的%X{X-B3-TraceId}。value需要唯一,並且不需要有業務含義,所以我這里直接使用UUID。接着AOP的proceedingJoinPoint.proceed()執行完后,我們的方法也就執行完了,要調用MDC.clear()把報錯到當前線程的MDC數據清空。

三、MDC原理

通過以上的實現,我們發現MDC使用起來非常簡單,就只有兩個步驟:

  • 1、定義日志格式,其中%X{}代表去MDC取值
  • 2、通過攔截器或者AOP在方法調用鏈最開始,設置MDC的值。

四、子線程MDC傳遞
  既然我們知道MDC底層使用TreadLocal來實現,那根據TreadLocal的特點,它是可以讓我們在同一個線程中共享數據的,但是往往我們在業務方法中,會開啟多線程來執行程序,這樣的話MDC就無法傳遞到其他子線程了。這時,我們需要使用額外的方法來傳遞存在TreadLocal里的值。MDC提供了一個叫getCopyOfContextMap的方法,很顯然,該方法就是把當前線程TreadLocal綁定的Map獲取出來,之后就是把該Map綁定到子線程中的ThreadLocal中了,具體代碼如下:

Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
new Thread(() -> {
if (copyOfContextMap != null) {
MDC.setContextMap(copyOfContextMap);
}
log.info("這個是子線程的信息");
}).start();
也就是說,我們在主線程中獲取MDC的值,然后在子線程中設置進去,這樣,子線程打印的信息也會帶有整個調用鏈共同的traceId了。
————————————————
版權聲明:本文為CSDN博主「不寫BUG的瑾大大」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/a183400826/article/details/101519219


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM