本项目演示如何在微服务架构中使用 MDC (Mapped Diagnostic Context) 实现分布式日志追踪,并解决异步线程中的上下文传递问题。
trace_id
串联跨服务请求日志。@Async
等异步场景下的上下文传递。trace_id
到下游服务。TransmittableThreadLocal
避免内存泄漏。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>transmittable-thread-localartifactId>
<version>2.14.2version>
dependency>
package org.example;
import org.slf4j.MDC;
import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.HashMap;
import java.util.Map;
public class TtlMDCAdapter {
private static final TransmittableThreadLocal<Map<String, String>> context = new TransmittableThreadLocal<Map<String, String>>() {
// 复制父线程的上下文
@Override
public Map<String, String> copy(Map<String, String> parentValue) {
return parentValue != null ? new HashMap<>(parentValue) : null;
}
// 子线程执行任务前:恢复 MDC 上下文
@Override
protected void beforeExecute() {
super.beforeExecute();
Map<String, String> map = get();
if (map != null) {
MDC.setContextMap(map); // 同步到 MDC
}
}
// 子线程任务执行后:清理 MDC 上下文
@Override
protected void afterExecute() {
super.afterExecute();
MDC.clear();
}
};
public static void put(String key, String value) {
Map<String, String> map = context.get();
if (map == null) {
map = new HashMap<>();
context.set(map);
}
map.put(key, value);
MDC.put(key, value); // 主线程同步到 MDC
}
public static String get(String key) {
Map<String, String> map = context.get();
return map != null ? map.get(key) : null;
}
public static void clear() {
context.remove();
MDC.clear();
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/**");
}
}
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String traceId = Optional.ofNullable(request.getHeader("X-Trace-Id"))
.orElse(UUID.randomUUID().toString());
TtlMDCAdapter.put("trace_id", traceId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
TtlMDCAdapter.clear();
}
}
public class FeignClientConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return template -> {
String traceId = TtlMDCAdapter.get("trace_id");
if (traceId != null) {
template.header("X-Trace-Id", traceId);
}
};
}
}
// Feign 客户端使用
@FeignClient(name = "user-service", configuration = FeignClientConfig.class)
public interface UserServiceClient {
@GetMapping("/user")
String getUserInfo();
}
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(runnable ->
() -> {
Map<String, String> context = TtlMDCAdapter.getCopyOfContextMap();
TtlMDCAdapter.setContextMap(context);
runnable.run();
TtlMDCAdapter.clear();
});
return executor;
}
}
@Service
public class OrderService {
@Async
public void asyncProcess() {
log.info("Async task with trace_id"); // 将正确携带 trace_id
}
}
// 使用 TTL 包装的线程池
private static final ExecutorService executor =
TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(10));
public void submitTask() {
executor.submit(() -> {
log.info("Thread pool task"); // trace_id 自动传递
});
}
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [trace_id=%X{trace_id}] %-5level %logger{36} - %msg%npattern>
encoder>
appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
root>
configuration>
现象 | 原因 | 解决方案 |
---|---|---|
日志中无 trace_id |
拦截器未正确注册 | 检查 WebMvcConfig 配置 |
异步任务丢失上下文 | 未使用 TTL 线程池 | 用 TtlExecutors 包装线程池 |
Feign 调用未传递 ID | 未配置 Feign 拦截器 | 检查 FeignClientConfig |
内存泄漏 | 未调用 clear() |
确保 finally 块中清理上下文 |
trace_id
trace_id
trace_id
在 ELK 或 Loki 中聚合日志