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> -
配置拦截器
javapublic 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()); } }