MDC 的使用及原理简析

1,072 阅读14分钟

当我们系统发生了问题,第一件事就是翻看日志。如果你在程序的关键位置合理地打印了日志,那么往往通过对这些日志的分析,就能很快定位问题进而解决问题。

然而,有些时候,即便打印了日志,但是在真正问题发生的时候,发现日志东一块西一块,难以拼凑出完整地链路。

例如我根据某个问题的关键词,定位到了对应的日志信息,下一步很可能想要看一下这个日志信息的上下文。

在 Splunk 的帮助下,我是这么做的:

  1. 修改时间筛选条件为对应日志信息的前后 5 秒。
  2. 修改查询关键词为对应的日志信息的线程ID。
  3. 不断调整日志信息的时间窗口,直到看到我想要的日志上下文信息

可以看到, 即便有 Splunk 的帮助,这个过程依然笨拙繁琐。并且如果服务的访问量大,日志多,即便把日志的时间窗口调整到几秒,也会有大量的干扰数据。

那有没有什么方法,可以让我们快速的将整个执行链路筛出来呢? 可以借助 MDC 技术。

MDC 简介

Mapped Diagnostic Context(MDC)是一种在多线程环境中用于增强日志记录的功能,它允许开发人员将与当前线程相关的特定诊断信息与日志条目关联起来。MDC的核心实现通常依赖于java.lang.ThreadLocal类,为每个线程提供独立的存储空间,实现线程安全的数据隔离。

快速入门

/**
 * MDC 入门示例
 *
 * @author hanxry
 */
public class SimpleDemo {
    private static final Logger logger = LoggerFactory.getLogger(TheadDemo.class);

    public static final String TRACE_ID = "traceId";

    public static void main(String[] args) {
        MDC.put(TRACE_ID, UUID.randomUUID().toString());
        logger.info("开始调用,进行业务处理");
        logger.info("处理完毕,释放空间,避免内存泄露");
        System.out.println(MDC.get(TRACE_ID));
        MDC.remove(TRACE_ID);
        logger.info("TRACE_ID 还在吗? {}", MDC.get(TRACE_ID) != null);

    }
}

配置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] [demo:%X{traceId}] - %m%n</Pattern>
        </layout>
    </appender>
    <root level="debug">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

运行结果:

多线程

/**
 * @author hanxry
 */
class MDCThread extends Thread {

    private static final Logger logger = LoggerFactory.getLogger(TheadDemo.class);
    public static final String TRACE_ID = "traceId";

    private final String flag;

    public MDCThread(String flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        MDC.put(TRACE_ID, UUID.randomUUID().toString());
        logger.info("开始调用服务{}", flag);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            logger.info(e.getMessage());
        }
        logger.info("服务{}处理完毕", flag);
        MDC.remove(TRACE_ID);
    }
}

运行结果:

小结

通过案例我们可以看到,MDC的主要功能包括:

  1. 关联上下文信息:允许将自定义信息存入 MDC,如用户ID、事务ID等信息与日志条目关联,丰富日志内容。
  2. 线程绑定:MDC与当前执行线程紧密关联,确保每个线程可以拥有独立的上下文信息。
  3. 自动注入日志消息:配置好日志格式后,日志框架会在生成日志时自动提取MDC中的键值对并插入到日志模板中。

原理分析

同 Slf4j 的其他组件一样,MDC 同样顶层定义接口,具体实现由底层去做。

我们主要看三个类

MDC (slf4j-api)

/**
 * 这个类隐藏并替代底层的日志系统提供MDC的功能
 * 如果底层日志系统实现了MDC的功能,则Slf4j的MDC,也就是当前这个类,将代理底层日志系统的MDC实现。
 * 
 *注意:只有log4j and logback 这两个日志系统提供了MDC的功能。java.util.logging 不支持,它使用 BasicMDCAdapter。
 * 其他的日志系统,slf4j-simple、slf4j-nop 使用的是 NOPMDCAdapter
 * 因此,作为一个 SLF4J 用户,你可以在 log4j、Logback 或 java.util.logging 存在的情况下利用 MDC,但无需将这些系统作为依赖项强加给你的用户。
 *
 * 注意,这个类提供的所有方法都是静态的
 */
public class MDC {
    static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";
    private static final String MDC_APAPTER_CANNOT_BE_NULL_MESSAGE = "MDCAdapter cannot be null. See also " + NULL_MDCA_URL;
    static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";
    /**
     * 这里我们注意,它的变量修饰符没有private
     * public 所有类都能访问
     * protect 当前类、子孙类、同包
     * 默认 当前类、同包
     * private 只有当前类能访问
     
     * 作者设置没有设置private, 意思是想改可以改。 
     * 并且通过后面的代码可以看到,直接改可能并不会生效。因为static{} 代码块里会再次赋值。
     */
    static MDCAdapter mdcAdapter;

    /**
     * 通过Git log 可以看到,这个静态内部类以及下方的 putCloseable 方法,都是14年添加的,给
     * try-with-resources语句 用的。
     *
     * try(MDC.MDCCloseable closeable = MDC.putCloseable(key, value)) {
     * ....
     * }
     *
     * 如果没有这个方法,那么需要时刻记着在 finally 中调用 MDC.remove()
     * 否则有可能会导致线程泄露
     */
    public static class MDCCloseable implements Closeable {
        private final String key;

        private MDCCloseable(String key) {
            this.key = key;
        }

        public void close() {
            MDC.remove(this.key);
        }
    }

    // 构造方法私有
    private MDC() {
    }

    /**
     *  静态代码块,用于加载一些配置。类加载时执行,只执行一次
     *  类加载时,加载-连接-初始化:初始化的时候,1.静态变量赋值 2.静态代码块执行
     *  静态代码块 的意义就在于给静态变量赋值。
     */
    static {
        /**
        * 这段代码的作用是从 provider 中获取 mdcAdapter ,
        * 也就是说,slf4j的实现者,要提供这个mdcAdapter的实现
        * 而 Provider的获取,实际上是应用了 java的 SPI 机制。
        */
        SLF4JServiceProvider provider = LoggerFactory.getProvider();
        if (provider != null) {
            // obtain and attach the MDCAdapter from the provider
            // If you wish to change the adapter, Setting the MDC.mdcAdapter variable might not be enough as
            // the provider might perform additional assignments that you would need to replicate/adapt.
            mdcAdapter = provider.getMDCAdapter();
        } else {
            /**
             * 如果获取不到 provider,咱也是提供了默认实现的。NOPMDCAdapter
             * 实际上,一般不会存在获取不到 provider的情况,因为 provider 也提供了默认的实现 NOP_FallbackServiceProvider
             * 从 NOP_FallbackServiceProvider 中获取到的 mdcAdapter 也是 NOPMDCAdapter
             */
            Reporter.error("Failed to find provider.");
            Reporter.error("Defaulting to no-operation MDCAdapter implementation.");
            mdcAdapter = new NOPMDCAdapter();
        }
    }

    public static void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }
        if (mdcAdapter == null) {
            throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE);
        }
        mdcAdapter.put(key, val);
    }

    public static MDCCloseable putCloseable(String key, String val) throws IllegalArgumentException {
        put(key, val);
        return new MDCCloseable(key);
    }

    public static String get(String key) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }

        if (mdcAdapter == null) {
            throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE);
        }
        return mdcAdapter.get(key);
    }
    
    public static void remove(String key) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }

        if (mdcAdapter == null) {
            throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE);
        }
        mdcAdapter.remove(key);
    }

    public static void clear() {
        if (mdcAdapter == null) {
            throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE);
        }
        mdcAdapter.clear();
    }

    /**
     * 获取当前线程MDC 的一份副本
     */
    public static Map<String, String> getCopyOfContextMap() {
        if (mdcAdapter == null) {
            throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE);
        }
        return mdcAdapter.getCopyOfContextMap();
    }

    public static void setContextMap(Map<String, String> contextMap) {
        if (mdcAdapter == null) {
            throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE);
        }
        mdcAdapter.setContextMap(contextMap);
    }

    public static MDCAdapter getMDCAdapter() {
        return mdcAdapter;
    }


    /**
     * 一个键对应多个值  2022年才加的方法
     */
    static public void pushByKey(String key, String value) {
        if (mdcAdapter == null) {
            throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE);
        }
        mdcAdapter.pushByKey(key, value);
    }

    static public String popByKey(String key) {
        if (mdcAdapter == null) {
            throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE);
        }
        return mdcAdapter.popByKey(key);
    }
    
    public Deque<String> getCopyOfDequeByKey(String key) {
        if (mdcAdapter == null) {
            throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE);
        }
        return mdcAdapter.getCopyOfDequeByKey(key);
    }
}

可以看到,这个类在做的事情主要有两个,一个校验 mdcAdapter !=null,而是把调用底层 mdcAdapter 的方法。

问题:NOPMDCAdapter 的作用是什么呢?

我觉得这是空对象模式得一种应用。如作者注释中所述:"你可以在 log4j、Logback 或 java.util.logging 存在的情况下利用 MDC,但无需将这些系统作为依赖项强加给你的用户。"

MDCAdapter (slf4j-api)

MDCAdapter只是定义了一个接口,具体实现由子类完成,源码如下:

public interface MDCAdapter {
    public void put(String key, String val);
    public String get(String key);
    public void remove(String key);
    public void clear();
    public Map<String, String> getCopyOfContextMap();
    public void setContextMap(Map<String, String> contextMap);
    public void pushByKey(String key, String value);
    public String popByKey(String key);
    public Deque<String> getCopyOfDequeByKey(String key);
    public void clearDequeByKey(String key);
}

它有四个实现类,BasicMDCAdapter、Reload4jMDCAdapter LogbackMDCAdapter,NOPMDCAdapter。Logback使用的是LogbackMDCAdapter。

LogbackMDCAdapter (logback)

public class LogbackMDCAdapter implements MDCAdapter {

    // BEWARE: Keys or values placed in a ThreadLocal should not be of a type/class
    // not included in the JDK. See also https://jira.qos.ch/browse/LOGBACK-450
    /**
     * 这个是当时有人用null 作key, 然后不会被垃圾回收,资源泄露出问题。 
     * 后来就禁止用null 做key了。可能作者为了以防万一,加了这个注释提示一下使用者
     */
    final ThreadLocal<Map<String, String>> readWriteThreadLocalMap = new ThreadLocal<Map<String, String>>();
    final ThreadLocal<Map<String, String>> readOnlyThreadLocalMap = new ThreadLocal<Map<String, String>>();
    private final ThreadLocalMapOfStacks threadLocalMapOfDeques = new ThreadLocalMapOfStacks();

    /**
     * 每次添加一个值时,都会创建一个新的 map 实例。
     * 这是为了确保序列化过程将操作更新后的 map,而不是发送旧 map 的引用,
     * 从而使远程 logback 组件不被最新的更改所影响。
     *
     * @throws IllegalArgumentException in case the "key" parameter is null
     */
    public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        }
        Map<String, String> current = readWriteThreadLocalMap.get();
        if (current == null) {
            current = new HashMap<String, String>();
            readWriteThreadLocalMap.set(current);
        }
        current.put(key, val);
        // 把旧 map 清掉
        nullifyReadOnlyThreadLocalMap();
    }

    /**
     * This method has no side effects.
     * 这个方法没有任何副作用
     */
    @Override
    public String get(String key) {
        Map<String, String> hashMap = readWriteThreadLocalMap.get();

        if ((hashMap != null) && (key != null)) {
            return hashMap.get(key);
        } else {
            return null;
        }
    }
    
    @Override
    public void remove(String key) {
        if (key == null) {
            return;
        }

        Map<String, String> current = readWriteThreadLocalMap.get();
        if (current != null) {
            current.remove(key);
            nullifyReadOnlyThreadLocalMap();
        }
    }

    private void nullifyReadOnlyThreadLocalMap() {
        readOnlyThreadLocalMap.set(null);
    }

    @Override
    public void clear() {
        readWriteThreadLocalMap.set(null);
        nullifyReadOnlyThreadLocalMap();
    }

    /**
     * <p>Get the current thread's MDC as a map. This method is intended to be used
     * internally.</p>
     * <p>
     * The returned map is unmodifiable (since version 1.3.2/1.4.2).
     */
    @SuppressWarnings("unchecked")
    public Map<String, String> getPropertyMap() {
        Map<String, String> readOnlyMap = readOnlyThreadLocalMap.get();
        if (readOnlyMap == null) {
            Map<String, String> current = readWriteThreadLocalMap.get();
            if (current != null) {
                final Map<String, String> tempMap = new HashMap<String, String>(current);
                readOnlyMap = Collections.unmodifiableMap(tempMap);
                readOnlyThreadLocalMap.set(readOnlyMap);
            }
        }
        return readOnlyMap;
    }

    /**
     * Return a copy of the current thread's context map. Returned value may be
     * null.
     */
    public Map getCopyOfContextMap() {
        Map<String, String> readOnlyMap = getPropertyMap();
        if (readOnlyMap == null) {
            return null;
        } else {
            return new HashMap<String, String>(readOnlyMap);
        }
    }

    /**
     * Returns the keys in the MDC as a {@link Set}. The returned value can be
     * null.
     */
    public Set<String> getKeys() {
        Map<String, String> readOnlyMap = getPropertyMap();

        if (readOnlyMap != null) {
            return readOnlyMap.keySet();
        } else {
            return null;
        }
    }

    /**
     * @param contextMap 
     * 为了给子线程设置 contextMap
     */
    @SuppressWarnings("unchecked")
    public void setContextMap(Map contextMap) {
        if (contextMap != null) {
            readWriteThreadLocalMap.set(new HashMap<String, String>(contextMap));
        } else {
            readWriteThreadLocalMap.set(null);
        }
        nullifyReadOnlyThreadLocalMap();
    }


    @Override
    public void pushByKey(String key, String value) {
        threadLocalMapOfDeques.pushByKey(key, value);
    }

    @Override
    public String popByKey(String key) {
        return threadLocalMapOfDeques.popByKey(key);
    }

    @Override
    public Deque<String> getCopyOfDequeByKey(String key) {
        return threadLocalMapOfDeques.getCopyOfDequeByKey(key);
    }

    @Override
    public void clearDequeByKey(String key) {
        threadLocalMapOfDeques.clearDequeByKey(key);
    }

}

整个的实现其实是很简单和清晰的,读起来并不复杂。我在阅读源码的过程中有几个问题:

  1. 为什么要定义两个 Map 呢?readWriteThreadLocalMap 和 readOnlyThreadLocalMap?

有个专业的词,描述这个行为: "COW"—— 写时复制。这是一项成熟的技术方案,用以解决多个线程获取相同资源时的协同问题。

这里的处理方式时,每次写操作,清空 readOnly Map。每次读操作,判断 readOnly 是否为空,如果为空则从主 Map 复制一份。readOnly 副本只能读,不允许修改。

目的:

  1. 是防止 ConcurrentModificationException

  2. 是为了防止内存泄漏。why? 这个是因为资源适合线程绑定的,当本线程变量被其他线程直接引用时,即便线程得操作已经完成,那么资源还是不能回收。

  3. 看起来 LogbackMDCAdapter 就是直接实现的接口,并没有适配什么东西,那么为什么要用适配器模式呢?

一般先有实现,后定义的规范,才需要适配器模式。

追溯一下 Git Log 可以看到 logback 是06年添加的MDC,并且在 07年改名字叫 LogbackMDCAdapter。

07 年是 slf4j 的诞生之年。log4j 最早在01年就添加了MDC。这几个日志框架 MDC 的作者全部都是 Ceki Gulcu。

所以,可以推测,作者是在 logback 的基础上开发的 slf4j 这个日志门面。而适配器模式,主要是去适配的 log4j 等其他更早出现的日志系统。logback 的 MDC 改名 LogbackMDCAdapter 是为了统一命名。

  1. 关于 Slf4j 日志门面的这个说法的一点疑问?

很多的说法都会把 slf4j 拿出来作为门面模式的典型案例。然而,把其归为门面模式的案例似乎有些牵强。首先,众所周知,我们在使用一种设计模式的时候,往往会在名称上体现出来,然而整个代码中找不到Facade单词。

其次,门面模式的标准类图可以看出来,其是把多个子系统的复杂操作合并到一起,进而简化应用。

然而,Slf4j 做的是什么呢?提供标准接口,用户可以自己选择不不同的实现。 其实就是典型的面向接口编程而已,并没有门面一说。

生产应用

在过滤器、拦截器或 AOP 中配置

这里举得例子是 AOP,我个人觉得 Filter 中比较好,一般日志这种需求,需要在访问过程的最前方进行拦截。

@Component
@Aspect
@Order(Ordered.HIGHEST_PRECEDENCE) // 一般 MDC 的优先级要放在最高
@AllArgsConstructor
public class MdcAspect {
    public static final String logIdKey = "logId";

    private static final Logger log = LoggerFactory.getLogger(MdcAspect.class);

    private final HttpServletRequest httpServletRequest;

    @Around(
            "@annotation(org.springframework.web.bind.annotation.GetMapping) " +
            "|| @annotation(org.springframework.web.bind.annotation.PostMapping) " +
            "|| @annotation(org.springframework.web.bind.annotation.PutMapping) " +
            "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping) "
    )
    public Object injectLogId(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        String logId = httpServletRequest.getHeader(MdcAspect.logIdKey);
        logId = StringUtils.isNotBlank(logId) ? (logId + "_") : "" + UUID.randomUUID();
        MDC.put(logIdKey, logId);
        log.info("inject logId = {}", logId);
        try {
            return proceedingJoinPoint.proceed();
        } finally {
            MDC.remove(logIdKey);
        }
    }
}

子线程中 MDC 传递

我们知道 MDC 底层使用 ThreadLocal 来实现,根据 ThreadLocal 的特点,它是可以让我们在同一个线程中共享数据的,但是往往我们在业务方法中,会开启多线程来执行程序,这样的话 MDC 就无法传递到其他子线程了。这时我们需要使用额外的方法来传递存在 ThreadLocal 里的值。MDC 提供了一个叫 getCopyOfContextMap 的方法,该方法就是把当前线程 ThreadLocal 绑定的 Map 获取出来,之后就是把该 Map 绑定到子线程中的ThreadLocal 中了,具体代码如下:

Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
new Thread(() -> {
    if (copyOfContextMap != null) {
        MDC.setContextMap(copyOfContextMap);
    }
    log.info("这个是子线程的信息");
}).start();

也就是说,我们在主线程中获取MDC的值,然后在子线程中设置进去,这样,子线程打印的信息也会带有整个调用链共同的 traceId 了。

线程池封装类 ThreadPoolExecutorMdcWrapper.java
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }
    
    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
                                        RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> Future<T> submit(Runnable task, T result) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
    }

    @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()));
    }
}

说明:

  • 继承ThreadPoolExecutor类,重新执行任务的方法
  • 通过ThreadMdcUtil对任务进行一次包装

线程traceId封装工具类:ThreadMdcUtil.java

public class ThreadMdcUtil {
    public static void setTraceIdIfAbsent() {
        if (MDC.get(Constants.TRACE_ID) == null) {
            MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());
        }
    }

    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();
            }
        };
    }
}

说明【以封装Runnable为例】:

  • 判断当前线程对应MDC的Map是否存在,存在则设置
  • 设置MDC中的traceId值,不存在则新生成,针对不是子线程的情况,如果是子线程,MDC中traceId不为null
  • 执行run方法

重新返回的是包装后的Runnable,在该任务执行之前【runnable.run()】先将主线程的Map设置到当前线程中【 即MDC.setContextMap(context)】,这样子线程和主线程MDC对应的Map就是一样的了

如果项目使用 ThreadPoolTaskExecutor

可以实现 TaskDecorator 进行扩展

public class MDCTaskDecorator implements TaskDecorator {
    @Override
    public @NonNull Runnable decorate(@NonNull Runnable runnable) {
        // 此时获取的是父线程的上下文数据
        Map<String, String> contextMap = MDC.getCopyOfContextMap();
        return () -> {
            try {
                if (!ObjectUtils.isEmpty(contextMap)) {
                    String logId = contextMap.getOrDefault(MdcAspect.logIdKey, UUID.randomUUID().toString());
                    logId += "_" + Thread.currentThread().getName();
                    contextMap.put(MdcAspect.logIdKey, logId);
                    // 内部为子线程的领域范围,所以将父线程的上下文保存到子线程上下文中,
                    // 而且每次submit/execute调用都会更新为最新的上下文对象
                    MDC.setContextMap(contextMap);
                }
                runnable.run();
            } finally {
                // 清除子线程的,避免内存溢出,就和ThreadLocal.remove()一个原因
                MDC.clear();
            }
        };
    }
}

@Configuration
public class PoolConfig {
    @Bean("taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //配置核心线程数
        executor.setCorePoolSize(5);
        //配置最大线程数
        executor.setMaxPoolSize(10);
        //配置队列大小
        executor.setQueueCapacity(100);
        //配置线程池中的线程的名称前缀
        executor.setThreadNamePrefix("mdc-trace-");
        // 异步MDC
        executor.setTaskDecorator(new MDCTaskDecorator());
        //执行初始化
        executor.initialize();
        return executor;
    }
}

HTTP 调用传递

如果需要记录 HTTP 调用过程的链路,那么可以在 header 中添加 traceId,同时在 MDC 的拦截器获取 header 中的 traceId 添加到 MDC 中。

  • 实现HttpClient拦截器
public class HttpClientTraceIdInterceptor implements HttpRequestInterceptor {
    @Override
    public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
        String traceId = MDC.get(Constants.TRACE_ID);
        if (traceId != null) {
            httpRequest.addHeader(Constants.TRACE_ID, traceId);
        }
    }
}

实现HttpRequestInterceptor接口并重写process方法

如果调用线程中含有traceId,则需要将获取到的traceId通过request中的header向下透传下去

  • 为HttpClient添加拦截器
private static CloseableHttpClient httpClient = HttpClientBuilder.create()
            .addInterceptorFirst(new HttpClientTraceIdInterceptor())
            .build();
  • MDC 过滤器
@Component
@WebFilter(
        filterName = "traceFilter",
        urlPatterns = {"/*"},
        asyncSupported = true)
public class MDCFilter implements Filter {
    public MDCFilter() {
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        // 从请求头获取trace_id,如果有,则加入其中
        String traceId = httpRequest.getHeader("traceId");
        if (StrUtil.isEmpty(traceId)) {
            traceId = UUID.randomUUID().toString().replaceAll("-", "");
        }
        MDC.put("traceId", traceId);
        // 分布式调用时,可以记录rpcId
        String xxRpcId = httpRequest.getHeader("xxRpcId");
        if (StrUtil.isNotEmpty(xxRpcId)) {
            MDC.put("rpcId", xxRpcId);
        }
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        MDC.remove("traceId");
    }
}