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

近期用到阿里的一款開源的數據同步工具 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 精心為你準備的編程進階資料。

Tags: