MDC是什麼鬼?用法、源碼一鍋端

近期用到阿里的一款開源的數據同步工具 Canal,不經意之中看到了 MDC 的用法,而且平時項目中也多次用到 MDC,趁機科普一把。

通過今天的分享,能讓你輕鬆 get 如下幾點,絕對收穫滿滿。

1. MDC 快速入門;

2. MDC 源碼解讀;

3. MDC 能幹什麼?

阿里開源項目 Canal:

MDC是什麼鬼?用法、源碼一鍋端

老項目這麼用過:

MDC是什麼鬼?用法、源碼一鍋端

但是無論怎麼用,都逃不過 MDC API 的使用,下面先花一分鐘快速入門,然後再逐步去深入 MDC。

1. MDC 快速入門

MDC 全稱是 Mapped Diagnostic Context,可以粗略的理解成是一個線程安全的存放診斷日誌的容器。

首先看看 MDC 基本的 API 的用法,能拋代碼的就不多廢話(根據 logback 官方案例改編)。

<code>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);
    }
}/<code>

代碼編寫完,貌似只有 MDC.put(K,V) 、MDC.remove(K) 兩句是陌生的,先不著急解釋它,等案例跑完就懂了,咱們繼續往下看。

接下來配置 logback.xml,通過 %X{REQ_ID} 來打印 REQ_ID 的信息,logback.xml 文件內容如下。

<code>

    
        
            [%t] [%X{REQ_ID}] - %m%n
        
    
    
        
    
/<code>

引入依賴包,讓程序快點跑起來看看效果。

<code>
    
        org.slf4j
        slf4j-api
        1.7.7
    
    
        ch.qos.logback
        logback-core
        1.2.3
    
    
        ch.qos.logback
        logback-access
        1.2.3
    
    
        ch.qos.logback
        logback-classic
        1.2.3
    
/<code>

程序跑起來,輸出截圖如下。


MDC是什麼鬼?用法、源碼一鍋端

根據輸出結果分析,能夠得到兩條結論。

第一:如圖中紅色圈住部分所示,當 logback 內置的日誌字段不能滿足業務需求時,便可以藉助 MDC 機制,將業務上想要輸出的信息,通過 logback 給打印出來;

第二:如藍色圈住部分所示,當調用 MDC.remove(Key) 後,便可將業務字段從 MDC 中刪除,日誌中就不再打印請求 ID 啦;

趁熱打鐵,我們迅速看看在多線程情況下,使用 MDC 會發生什麼現象呢?

還是基於上面的代碼,把代碼段放到了線程體內,稍微進行改造了一下,代碼如下。

<code>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);
    }
}/<code>

程序跑起來看看效果。

MDC是什麼鬼?用法、源碼一鍋端

依據程序輸出進行分析,能夠看到線程 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,翻譯過來就是「一套簡單的日誌門面」。是為了讓研發人員在項目中切換日誌組件的方便,特意抽象出的一層。

項目開發中經常這麼定義日誌對象:

<code>Logger logger = LoggerFactory.getLogger(SimpleMDC.class)/<code>

其中 Logger 就來自於 SLF4J 的規範包,項目中一旦這樣定義 Logger,在底層就可以無縫切換 logback、log4j 等日誌組件啦,這或許就是 Java 為什麼要提倡要面向接口編程的好處。

見證奇蹟的時刻要到了,下面就好好揭秘一下 MDC 背後藏著什麼東東?

首先通過 org.slf4j.MDC 的源碼,可以很清楚的知道 MDC 主要是通過 MDCAdapter 來完成 put、get、remove 等操作。

MDC是什麼鬼?用法、源碼一鍋端

不出所料 MDCAdapter 也是個接口。在 Java 的世界裡,應該都知道定義接口的目的:就是為了定義規範,讓子類去實現。

MDCAdapter 就和 JDBC 的規範類似,專門用於定義操作規範。JDBC 是為了定義數據庫操作規範,讓數據庫廠商(MySQL、DB2、Oracle 等)去實現;而 MDCAdapter 則是讓具體的日誌組件(logback、log4j等)去實現。

MDC是什麼鬼?用法、源碼一鍋端

MDCAdapter 接口的實現類,有 NOPMDCAdapter、BasicMDCAdapter、LogbackMDCAdapter 以及 Log4jMDCAdapter 等等幾種,其中 log4j 使用的是 Log4jMDCAdapter,而 Logback 使用的是 LogbackMDCAdapter。

MDC是什麼鬼?用法、源碼一鍋端

本次重點說 LogbackMDCAdapter 的源碼,截圖如下。

MDC是什麼鬼?用法、源碼一鍋端

通過圖中標註 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. 寫在最後

若想 get 更多案例及用法,可參考 logback 官方鏈接。

<code>http://logback.qos.ch/manual/mdc.html/<code>

行文至此,接近尾聲,本次主要讓大家對 MDC 進行快速入門,並通過剖析源碼,窺探 MDC 的背後,最終分享了一些 MDC 在項目研發中能做什麼的實踐思路,歡迎大家多去嘗試實現。

另外,若是急需分佈式調用鏈路跟蹤、監控的輪子,在自研的輪子已經跟不上項目的發展時,有以下幾款開源的輪子推薦,不妨拿去一試。

MDC是什麼鬼?用法、源碼一鍋端

一起聊技術、談業務、噴架構,少走彎路,不踩大坑,歡迎繼續關注「一猿小講」,會持續輸出更多原創精彩分享!


分享到:


相關文章: