分布式链路追踪MDC+TraceId

1、MDC简述

MDC(Mapped Diagnostic Context)是一个映射,用于存储运行上下文的特定线程的上下文数据。因此,如果使用log4j进行日志记录,则每个线程都可以拥有自己的MDC,该MDC对整个线程是全局的。属于该线程的任何代码都可以轻松访问线程的MDC中存在的值。

2、MDC实现原理

MDC 是 SLF4J/Logback 提供的 线程级日志上下文存储。它内部通过 ThreadLocal<Map<String, String>> 保存上下文信息。

  • 当在某个线程里执行 MDC.put("traceId", "xxx") 时,traceId 会存入当前线程的 ThreadLocal 中。
  • 日志框架在输出日志时,会自动从 MDC 中获取 traceId 并填入日志模板。
  • 不同线程的 MDC 是独立的,每个线程都有自己的上下文,不会互相干扰。

3、MDC的API

java 复制代码
// 移除所有MDC
clear()
// 获取当前线程MDC中指定key的值
get (String key)
// 获取当前线程MDC的MDC
getContext()
// 往当前线程的MDC中存入指定的键值对
put(String key, Object o)
// 删除当前线程MDC中指定的键值对 。
remove(String key)

4、MDC实现方式

4.1、需要一个全服务唯一的id,即traceId

使用最简单的uuid即可

4.2、traceId如何在服务间传递?
  • xml 的日志格式中添加 %X{traceId} 配置

    java 复制代码
    <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
    	<layout class="org.apache.log4j.PatternLayout">
    		<param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%X{traceId}] [%p] %l[%t]%n%m%n"/>	
    	</layout>
    </appender>
  • HTTP请求处理

    java 复制代码
    @Component
    publicclassTraceIdFilterextendsOncePerRequestFilter{
        privatestaticfinal String TRACE_ID = "traceId";
    
        @Override
        protectedvoiddoFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {
            // 跳过预检请求(OPTIONS)
            if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
                filterChain.doFilter(request, response);
                return;
            }
    
            try {
                String traceId = UUID.randomUUID().toString().replace("-", "");
                MDC.put(TRACE_ID, traceId);
                filterChain.doFilter(request, response);
            } finally {
                MDC.remove(TRACE_ID);
            }
        }
    }
4.3、traceId如何在多线程中传递?

MDC底层使用TreadLocal来实现,那根据TreadLocal的特点,它是可以让我们在同一个线程中共享数据的,但是往往我们在业务方法中,会开启多线程来执行程序,这样的话MDC就无法传递到其他子线程了。这时,我们需要使用额外的方法来传递存在TreadLocal里的值

java 复制代码
publicvoidexecute(Runnable task){
    defaultThreadPoolExecutor.execute(wrap(task, MDC.getCopyOfContextMap()));
}


public <T> Future<T> submit(Callable<T> task){
    return defaultThreadPoolExecutor.submit(wrap(task, MDC.getCopyOfContextMap()));
} 

private Runnable wrap(Runnable task, Map<String, String> contextMap){
    return () -> {
        Map<String, String> previous = MDC.getCopyOfContextMap();
        if (contextMap != null) {
            MDC.setContextMap(contextMap);
        } else {
            MDC.clear();
        }
        try {
            task.run();
        } finally {
            // 恢复线程池线程原来的 MDC,避免影响下一次任务
            if (previous != null) {
                MDC.setContextMap(previous);
            } else {
                MDC.clear();
            }
        }
    };
}


private <T> Callable<T> wrap(Callable<T> task, Map<String, String> contextMap){
    return () -> {
        Map<String, String> previous = MDC.getCopyOfContextMap();
        if (contextMap != null) {
            MDC.setContextMap(contextMap);
        } else {
            MDC.clear();
        }
        try {
            return task.call();
        } finally {
            if (previous != null) {
                MDC.setContextMap(previous);
            } else {
                MDC.clear();
            }
        }
    };
}

5、项目实战

5.1、配置日志
  • 配置log

    java 复制代码
    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="INFO" name="common-logging">
        <Properties>
            <Property name="appName">${spring:spring.application.name:-unknown-app} </Property>
        </Properties>
    
        <Appenders>
            <!--打印出INFO级别日志,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
            <RollingFile name="RollingFileInfo" fileName="logs/${appName}-info.log"
                         filePattern="logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
                <Filters>
                    <!--只接受INFO级别的日志,其余的全部拒绝处理-->
                    <ThresholdFilter level="INFO"/>
                    <ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
                </Filters>
                <PatternLayout pattern="%d{DEFAULT} [%X{traceId}] %-5level %logger{36} - %X{X-Request-ID} - %msg%n"/>
                <Policies>
                    <!-- 每天滚动一次 -->
                    <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                    <!-- 文件大小超过50MB也滚动 -->
                    <SizeBasedTriggeringPolicy size="50 MB"/>
                </Policies>
                <DefaultRolloverStrategy max="10"/>
            </RollingFile>
    
            <!--打印出WARN级别日志,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
            <RollingFile name="RollingFileWarn" fileName="logs/${appName}-warn.log"
                         filePattern="logs/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log">
                <Filters>
                    <!--只接受WARN级别的日志,其余的全部拒绝处理-->
                    <ThresholdFilter level="WARN"/>
                    <ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/>
                </Filters>
                <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%X{traceId}] %-5level %class{36} %L %M - %msg%xEx%n"/>
                <Policies>
                    <!-- 每天滚动一次 -->
                    <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                    <!-- 文件大小超过50MB也滚动 -->
                    <SizeBasedTriggeringPolicy size="50 MB"/>
                </Policies>
                <DefaultRolloverStrategy max="10"/>
            </RollingFile>
    
            <!--处理error级别的日志,并把该日志放到logs/error.log文件中-->
            <RollingFile name="RollingFileError" fileName="logs/${appName}-error.log"
                         filePattern="logs/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log">
                <ThresholdFilter level="ERROR"/>
                <PatternLayout
                        pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%X{traceId}] %-5level %class{36} %L %M - %msg%xEx%n"/>
                <Policies>
                    <!-- 每天滚动一次 -->
                    <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                    <!-- 文件大小超过50MB也滚动 -->
                    <SizeBasedTriggeringPolicy size="50 MB"/>
                </Policies>
                <DefaultRolloverStrategy max="10"/>
            </RollingFile>
    
            <!-- 将日志信息从控制台输出 -->
            <Console name="Console" target="SYSTEM_OUT">
                <!--只接受程序中DEBUG级别的日志进行处理-->
                <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
                <PatternLayout pattern="%d{DEFAULT} [%X{traceId}] %-5level %logger{36} - %X{X-Request-ID} - %msg%n"/>
            </Console>
        </Appenders>
        <Loggers>
            <logger name="org.elasticsearch.plugins" level="INFO"/>
            <logger name="org.springframework" level="INFO"/>
            <asyncRoot level="INFO">
                <appender-ref ref="Console"/>
                <appender-ref ref="RollingFileInfo"/>
                <appender-ref ref="RollingFileWarn"/>
                <appender-ref ref="RollingFileError"/>
            </asyncRoot>
        </Loggers>
    </Configuration>
  • 配置拦截器

    java 复制代码
    public class TraceFeignInterceptor implements RequestInterceptor {
    
        private static final Logger log = LoggerFactory.getLogger(TraceFeignInterceptor.class);
    
        @Override
        public void apply(RequestTemplate template) {
            try {
                // 从 MDC 中获取 traceId
                String traceId = MDC.get(Constant.LOG_TRACE_ID);
    
                if (StringUtils.isNotBlank(traceId)) {
                    // 将 traceId 添加到 Feign 请求头中
                    template.header(Constant.TRACE_ID, traceId);
                    log.debug("Added traceId to Feign request header: {} = {}", Constant.TRACE_ID, traceId);
                } else {
                    log.debug("TraceId not found in MDC, skipping header addition");
                }
            } catch (Exception e) {
                // 拦截器异常不应该影响主业务流程
                log.warn("Failed to add traceId to Feign request header", e);
            }
        }
    }
  • fegin配置

    java 复制代码
    @Configuration
    @ConditionalOnClass(name = "org.springframework.cloud.openfeign.FeignClient")
    public class FeignErrorDecoderConfiguration {
        /**
         * Feign 请求拦截器 - 传递 TraceId
         * 自动在 Feign 调用时传递 traceId,实现微服务链路追踪
         * 默认启用,无需配置
         */
        @Bean
        @ConditionalOnClass(name = "feign.RequestInterceptor")
        public RequestInterceptor traceFeignInterceptor() {
            return new TraceFeignInterceptor();
        }
    }
5.2、配置线程池
  • 线程池配置中加入MDC

    java 复制代码
    @Configuration
    public class ThreadPoolConfig {
        /**
         * 订单处理线程池
         * @return
         */
        @Bean("orderExecutor")
        public Executor orderExecutor() {
            return createThreadPool(16, 32, "order-", 10L, 800);
        }
    
        /**
         * checkRates线程池
         * @return
         */
        @Bean("checkRatesExecutor")
        public Executor checkRatesExecutor() {
            return createThreadPool(Runtime.getRuntime().availableProcessors() + 1, Runtime.getRuntime().availableProcessors() + 1, "checkRates-",0L,200);
        }
    
        /**
         * bookingConfirmation线程池
         * @return
         */
        @Bean("bookingConfirmationExecutor")
        public Executor bookingConfirmationExecutor() {
            return createThreadPool(Runtime.getRuntime().availableProcessors() + 1, Runtime.getRuntime().availableProcessors() * 2, "bookingConfirmation-",10L,1000);
        }
    
        /**
         * cancelConfirm线程池
         * @return
         */
        @Bean("cancelConfirmExecutor")
        public Executor cancelConfirmExecutor() {
            return createThreadPool(Runtime.getRuntime().availableProcessors() + 1, Runtime.getRuntime().availableProcessors() * 2, "cancelConfirm-",10L,1000);
        }
    
        /**
         * changeBooking线程池
         * @return
         */
        @Bean("changeBookingExecutor")
        public Executor changeBookingExecutor() {
            return createThreadPool(Runtime.getRuntime().availableProcessors() + 1, Runtime.getRuntime().availableProcessors() * 2, "changeBooking-",10L,1000);
        }
    
    
        /**
         * orderQuery线程池
         * @return
         */
        @Bean("orderQueryExecutor")
        public Executor orderQueryExecutor() {
            return createThreadPool(Runtime.getRuntime().availableProcessors() + 1, Runtime.getRuntime().availableProcessors() + 1, "orderQuery-",0L,50);
        }
    
        /**
         * 邮件发送线程池(用于异步发送确认函邮件)
         * 核心线程数:CPU核心数
         * 最大线程数:CPU核心数 * 2
         * 队列容量:500(足够容纳大批量邮件发送任务)
         * @return
         */
        @Bean("emailSendingExecutor")
        public Executor emailSendingExecutor() {
            int cpuCount = Runtime.getRuntime().availableProcessors();
            return createThreadPool(cpuCount, cpuCount * 2, "email-sending-", 60L, 500);
        }
    
    
        private Executor createThreadPool(int corePoolSize, int maxPoolSize, String namePrefix, Long keepAliveTime, int queueCapacity) {
            // 创建基础 ThreadFactory
            ThreadFactory baseFactory = new ThreadFactoryBuilder().setNameFormat(namePrefix + "%d").build();
    
            // 包装 ThreadFactory 以支持 MDC 上下文传递(traceId等)
            ThreadFactory mdcAwareFactory = runnable -> {
                // 保存当前线程的 MDC 上下文
                Map<String, String> contextMap = MDC.getCopyOfContextMap();
    
                // 包装原始 Runnable
                Runnable wrappedRunnable = () -> {
                    try {
                        // 在新线程中恢复 MDC 上下文
                        if (contextMap != null) {
                            MDC.setContextMap(contextMap);
                        }
                        runnable.run();
                    } finally {
                        // 清理 MDC,防止内存泄漏
                        MDC.clear();
                    }
                };
    
                return baseFactory.newThread(wrappedRunnable);
            };
    
            return new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(queueCapacity),
                mdcAwareFactory,
                new ThreadPoolExecutor.CallerRunsPolicy());
        }
    }
相关推荐
这是程序猿19 分钟前
基于java的SpringBoot框架医院药品管理系统
java·开发语言·spring boot·后端·spring·医院药品管理系统
tkevinjd23 分钟前
IO流4(小练习)
java·io流
Vic1010124 分钟前
PostgreSQL 中序列(bigserial 和手动序列)的使用与注意事项
java·大数据·数据库·postgresql
Seven9724 分钟前
数据结构-堆
java
2503_9469718628 分钟前
【FullStack/ZeroDay】2026年度全栈魔法架构与分布式恶意节点清除基准索引 (Benchmark Index)
分布式·网络安全·架构·系统架构·区块链·数据集·全栈开发
回家路上绕了弯31 分钟前
Resilience4j全面指南:轻量级熔断限流框架的实战与落地
分布式·后端
SimonKing31 分钟前
你的网站SSL证书又要过期了?这个工具能让你永久告别焦虑
java·后端·程序员
CryptoRzz32 分钟前
印度交易所 BSE 与 NSE 实时数据 API 接入指南
java·c语言·python·区块链·php·maven·symfony
梵得儿SHI35 分钟前
SpringCloud 核心组件精讲:Sentinel 熔断限流全攻略-流量控制、熔断降级、热点参数限流(含 Dashboard 部署 + 项目集成实操)
java·spring cloud·sentinel·熔断降级·热点参数限流·微服务流量控制
麦兜*36 分钟前
Spring Boot 3.x 升级踩坑大全:Jakarta EE 9+、GraalVM Native 与配置迁移实战
java·spring boot·后端·spring·spring cloud