• 链路ID通过MDC实现线程间传递


    👽个人博客:https://everspring.github.io/
    👽公众号:爱历史的IT男

    一、了解MDC

    MDC(Mapped Diagnostic Context,映射调试上下文)是 slf4j提供的一种轻量级的日志跟踪工具。Log4j、Logback或者Log4j2等日志中最常见区分同一个请求的方式是通过线程名,而如果
    请求量大,线程名在相近的时间内会有很多重复的而无法分辨,因此引出了trace-id,即在接收到的时候生成唯一的请求id,在整个执行链路中带上此唯一id.
    MDC.java本身不提供传递traceId的能力,真正提供能力的是MDCAdapter接口的实现。比如Log4j的是Log4jMDCAdapter,Logback的是LogbackMDCAdapter

    二、MDC普通使用

    1. MDC.put(“trace-id”, traceId); // 添加traceId
    2. MDC.remove(“trace-id”); // 移除traceId
    3. 在日志配置文件中节点中以%X{trace-id}取出,比如:%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [uniqid-%X{trace-id}] %logger{50}-%line - %m%n
      以上方式,只能在做put操作的当前线程中获取到值。那是因为MDC的实现原理主要就是ThreadLocal,ThreadLocal只对当前线程有效。

    三、多线程中的MDC

    要破除ThreadLocal只对当前线程有线的方法有两种:
    一种是JDK自带的、ThreadLocal的扩展类InheritableThreadLocal,子线程会拷贝父线程中的变量值
    一种是引入alibabatransmittable-thread-local包的TransmittableThreadLocal实现

    据我所知,只有子线程获取父线程,不能父线程获取子线程的变量,如果有知道父获取子的方法,麻烦说一下,谢谢

    Sl4j本身的提供的BasicMDCAdapter就是基于InheritableThreadLocal实现了线程间传递,但log4j、logback这两个实际的日志实现没有提供,log4j2提供了但默认关闭。

    1. log4j2主要是根据isThreadContextMapInheritable变量控制的,有两种方法:
      • log4j2.component.properties文件中配置
      • 类型模块中定义,System.setProperty("log4j2.isThreadContextMapInherimeble", "true");
    2. lob4j和logback需要自己手动实现,主要有两种方法,一是手动在线程中的处理,一种是重写LogbackMDCAdapter。本文以在线程池中创建为例
    import org.slf4j.MDC;
    import org.springframework.util.CollectionUtils;
    
    import java.util.Map;
    import java.util.concurrent.Callable;
    
    /**
     * @desc: 定义MDC工具类,支持Runnable和Callable两种,目的就是为了把父线程的traceId设置给子线程
     */
    public class MdcUtil {
    
        public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
            return () -> {
                if (CollectionUtils.isEmpty(context)) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                try {
                    return callable.call();
                } finally {
                    // 清除子线程的,避免内存溢出,就和ThreadLocal.remove()一个原因
                    MDC.clear();
                }
            };
        }
    
        public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
            return () -> {
                if (CollectionUtils.isEmpty(context)) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                try {
                    runnable.run();
                } finally {
                    MDC.clear();
                }
            };
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    import java.util.concurrent.Callable;
    import java.util.concurrent.Future;
    
    /**
     * @desc: 把当前的traceId透传到子线程特意加的实现。重点就是 MDC.getCopyOfContextMap(),此方法获取当前线程(父线程)的traceId
     */
    public class ThreadPoolMdcExecutor extends ThreadPoolTaskExecutor {
        @Override
        public void execute(Runnable task) {
            super.execute(MdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    
        @Override
        public Future<?> submit(Runnable task) {
            return super.submit(MdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    
        @Override
        public <T> Future<T> submit(Callable<T> task) {
            return super.submit(MdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    
    import javax.annotation.Resource;
    import java.util.concurrent.RejectedExecutionHandler;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.ScheduledThreadPoolExecutor;
    
    /**
     * 线程池配置。重点就是把ThreadPoolTaskExecutor换成ThreadPoolMdcExecutor
     **/
    @Configuration
    public class ThreadPoolConfig {
    
        @Resource
        private ThreadPoolProperties threadPoolProperties;
    
        /**
         * 业务用到的线程池
         * @return
         */
        @Bean(name = "threadPoolTaskExecutor")
        public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolMdcExecutor();
            executor.setMaxPoolSize(threadPoolProperties.getMaxPoolSize());
            executor.setCorePoolSize(threadPoolProperties.getCorePoolSize());
            executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
            executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
            RejectedExecutionHandler handler = ReflectUtil.newInstance(threadPoolProperties.getRejectedExecutionHandler().getClazz());
            executor.setRejectedExecutionHandler(handler);
            return executor;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    推荐文章:

    1. https://www.cnblogs.com/yangyongjie/p/12523567.html
    2. https://blog.csdn.net/zlt2000/article/details/99641821
  • 相关阅读:
    C++(CMake)视觉OpenCV滤镜-自动光学检测-对象分类跟踪-背景建模和形态学操作-文本识别分割算法
    NCCL源码解析①:初始化及ncclUniqueId的产生
    金仓数据库KingbaseES客户端编程接口指南-ado.net(5. 连接串参数)
    设计模式——行为型
    easy_see
    【JavaWeb】案例一:用户注册
    ElasticSearch(二):ES集群安装
    华为“天才少年” DIY 生日礼物:四个月打造能缝葡萄的机械臂!
    diffusers库中stable Diffusion模块的解析
    Docker 安装HomeAssistant智能家居系统
  • 原文地址:https://blog.csdn.net/xuechangchun007/article/details/126502110