近期用到阿里的一款開源的數據同步工具 Canal,不經意之中看到了 MDC 的用法,而且平時項目中也多次用到 MDC,趁機科普一把。
通過今天的分享,能讓你輕松 get 如下幾點,絕對收獲滿滿。
a)MDC 快速入門;
b)MDC 源碼解讀;
c)MDC 能干什么?
阿里開源項目 Canal:
老項目這么用過:
但是無論怎么用,都逃不過 MDC API 的使用,下面先花一分鍾快速入門,然后再逐步去深入 MDC。
1. MDC 快速入門
MDC 全稱是 Mapped Diagnostic Context,可以粗略的理解成是一個線程安全的存放診斷日志的容器。
首先看看 MDC 基本的 API 的用法,能拋代碼的就不多廢話(根據 logback 官方案例改編)。
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import java.util.UUID; /** * MDC快速入門示例 * * @author 一猿小講 */ public class SimpleMDC { private static final Logger logger = LoggerFactory.getLogger(SimpleMDC.class); public static final String REQ_ID = "REQ_ID"; public static void main(String[] args) { MDC.put(REQ_ID, UUID.randomUUID().toString()); logger.info("開始調用服務A,進行業務處理"); logger.info("業務處理完畢,可以釋放空間了,避免內存泄露"); MDC.remove(REQ_ID); logger.info("REQ_ID 還有嗎?{}", MDC.get(REQ_ID) != null); } }
代碼編寫完,貌似只有 MDC.put(K,V) 、MDC.remove(K) 兩句是陌生的,先不着急解釋它,等案例跑完就懂了,咱們繼續往下看。
接下來配置 logback.xml,通過 %X{REQ_ID} 來打印 REQ_ID 的信息,logback.xml 文件內容如下。
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>[%t] [%X{REQ_ID}] - %m%n</Pattern> </layout> </appender> <root level="debug"> <appender-ref ref="CONSOLE"/> </root> </configuration>
引入依賴包,讓程序快點跑起來看看效果。
<dependencies> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-access</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> </dependencies>
程序跑起來,輸出截圖如下。
根據輸出結果分析,能夠得到兩條結論。
第一:如圖中紅色圈住部分所示,當 logback 內置的日志字段不能滿足業務需求時,便可以借助 MDC 機制,將業務上想要輸出的信息,通過 logback 給打印出來;
第二:如藍色圈住部分所示,當調用 MDC.remove(Key) 后,便可將業務字段從 MDC 中刪除,日志中就不再打印請求 ID 啦;
趁熱打鐵,我們迅速看看在多線程情況下,使用 MDC 會發生什么現象呢?
還是基於上面的代碼,把代碼段放到了線程體內,稍微進行改造了一下,代碼如下。
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import java.util.UUID; /** * MDC快速入門示例 * * @author 一猿小講 */ public class SimpleMDC { public static void main(String[] args) { new BizHandle("F0000").start(); new BizHandle("F9999").start(); } } class BizHandle extends Thread { private static final Logger logger = LoggerFactory.getLogger(SimpleMDC.class); public static final String REQ_ID = "REQ_ID"; private String funCode; public BizHandle(String funCode) { this.funCode = funCode; } @Override public void run() { MDC.put(REQ_ID, UUID.randomUUID().toString()); logger.info("開始調用服務{},進行業務處理", funCode); try { Thread.sleep(10000); } catch (InterruptedException e) { logger.info(e.getMessage()); } logger.info("服務{}處理完畢,可以釋放空間了,避免內存泄露", funCode); MDC.remove(REQ_ID); } }
程序跑起來看看效果。
依據程序輸出進行分析,能夠看到線程 Thread-0 與 Thread-1 在 MDC 中放入的 REQ_ID 的值是互不影響,也就是說 MDC 中的值是與線程綁定在一起的。
好了,入門程序就這么簡單,簡單做個小結。
a)MDC 提供的 put 方法,可以將一個 K-V 的鍵值對放到容器中,並且能保證同一個線程內,Key 是唯一的,不同的線程 MDC 的值互不影響;
b) 在 logback.xml 中,在 layout 中可以通過聲明 %X{REQ_ID} 來輸出 MDC 中 REQ_ID 的信息;
c)MDC 提供的 remove 方法,可以清除 MDC 中指定 key 對應的鍵值對信息。
通過快速入門的程序,得知 MDC 的值與線程是綁定在一起的,不同線程互不影響,MDC 背后到底是怎么實現的呢?不妨從源碼上看一看。
2. MDC 源碼解讀
解讀源碼之前,要提提 SLF4J,全稱是 Simple Logging Facade for Java,翻譯過來就是「一套簡單的日志門面」。是為了讓研發人員在項目中切換日志組件的方便,特意抽象出的一層。
項目開發中經常這么定義日志對象:
Logger logger = LoggerFactory.getLogger(SimpleMDC.class)
其中 Logger 就來自於 SLF4J 的規范包,項目中一旦這樣定義 Logger,在底層就可以無縫切換 logback、log4j 等日志組件啦,這或許就是 Java 為什么要提倡要面向接口編程的好處。
見證奇跡的時刻要到了,下面就好好揭秘一下 MDC 背后藏着什么東東?
首先通過 org.slf4j.MDC 的源碼,可以很清楚的知道 MDC 主要是通過 MDCAdapter 來完成 put、get、remove 等操作。
不出所料 MDCAdapter 也是個接口。在 Java 的世界里,應該都知道定義接口的目的:就是為了定義規范,讓子類去實現。
MDCAdapter 就和 JDBC 的規范類似,專門用於定義操作規范。JDBC 是為了定義數據庫操作規范,讓數據庫廠商(MySQL、DB2、Oracle 等)去實現;而 MDCAdapter 則是讓具體的日志組件(logback、log4j等)去實現。
MDCAdapter 接口的實現類,有 NOPMDCAdapter、BasicMDCAdapter、LogbackMDCAdapter 以及 Log4jMDCAdapter 等等幾種,其中 log4j 使用的是 Log4jMDCAdapter,而 Logback 使用的是 LogbackMDCAdapter。
本次重點說 LogbackMDCAdapter 的源碼,截圖如下。
通過圖中標注 1、2 的代碼,可以清晰的知道 MDC 底層最終使用的是 ThreadLocal 來進行的實現(水落石出,花落它家)。
a)ThreadLocal 很多地方叫做線程本地變量,也有些地方叫做線程本地存儲。
b)ThreadLocal 的作用是提供線程內的局部變量,這種變量在線程的生命周期內起作用,減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的復雜度。
c)ThreadLocal 使用場景為用來解決數據庫連接、Session 管理等。
本次不對 ThreadLocal 展開去說,若感興趣的可自行填補一下。
3. MDC 能干什么?
MDC 的應用場景其實蠻多的,下面簡單列舉幾個。
a)在 WEB 應用中,如果想在日志中輸出請求用戶 IP 地址、請求 URL、統計耗時等等,MDC 基本都能支撐;
b)在 WEB 應用中,如果能畫出用戶的請求到響應整個過程,勢必會快速定位生產問題,那么借助 MDC 來保存用戶請求時產生的 reqId,當請求完成后,再將這個 reqId 進行移除,這么一來通過 grep reqId 就能輕松 get 整個請求流程的日志軌跡;
c)在微服務盛行的當下,鏈路跟蹤是個難題,而借助 MDC 去埋點,巧妙實現鏈路跟蹤應該不是問題。
4. 寫在最后
行文至此,接近尾聲,本次主要讓大家對 MDC 進行快速入門,並通過剖析源碼,窺探 MDC 的背后,最終分享了一些 MDC 在項目研發中能做什么的實踐思路,歡迎大家多去嘗試實現。
另外,若是急需分布式調用鏈路跟蹤、監控的輪子,在自研的輪子已經跟不上項目的發展時,有以下幾款開源的輪子推薦,不妨拿去一試。
一起聊技術、談業務、噴架構,少走彎路,不踩大坑,歡迎繼續關注「一猿小講」,會持續輸出更多原創精彩分享!
可以微信搜索公眾號「 一猿小講 」回復「1024」get 精心為你准備的編程進階資料。