李锋镝的博客

  • 首页
  • 时间轴
  • 插件
  • 评论区显眼包🔥
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
  • 留言
  • 赞助
Destiny
自是人生长恨水长东
  1. 首页
  2. 后端
  3. 正文

SpringBoot日志链路追踪深度实战:基于 TraceId 打通全链路日志

2025年11月5日 83点热度 0人点赞 0条评论

在复杂业务系统中,一个用户请求可能会贯穿多个服务、调用数十个方法、涉及多线程异步处理——排查问题时,日志被不同请求穿插得杂乱无章,想要找到某条请求的完整链路日志如同大海捞针。

TraceId 日志链路追踪正是为解决这个痛点而生:通过为每一次请求分配唯一的 TraceId,将该请求在全链路中的所有日志串联起来,让排查问题从“拼凑日志”变成“精准定位”。本文将基于 SpringBoot 实现从基础到进阶的 TraceId 链路追踪,覆盖同步请求、异步线程、定时任务、微服务调用等全场景,提供生产级可直接落地的方案。

一、核心原理:TraceId 如何打通全链路?

1. 核心组件与流程

TraceId 链路追踪的核心是“ThreadLocal + MDC + 拦截器/AOP”,流程如下:

  1. 请求入口拦截:通过拦截器(Web 请求)或 AOP(定时任务)为每次请求生成唯一 TraceId;
  2. 上下文存储:将 TraceId 存入 MDC(Mapped Diagnostic Context,日志诊断上下文),MDC 底层基于 ThreadLocal,确保线程隔离;
  3. 日志打印:通过日志框架(Logback/Log4j2)配置,自动在日志中打印 TraceId;
  4. 链路传递:同步调用时,ThreadLocal 自动传递 TraceId;异步调用/跨服务调用时,手动传递 TraceId;
  5. 资源清理:请求结束后,清除 MDC 中的 TraceId,避免内存泄漏。

2. MDC 核心 API 说明

MDC 是 SLF4J 提供的日志上下文工具,核心 API 简单易用:

API 方法 作用 示例
MDC.put(String key, String value) 向当前线程的 MDC 存入键值对 MDC.put("traceId", UUID.randomUUID().toString())
MDC.get(String key) 获取当前线程 MDC 中指定 key 的值 String traceId = MDC.get("traceId")
MDC.getCopyOfContextMap() 复制当前线程的 MDC 上下文(用于异步传递) Map<String, String> context = MDC.getCopyOfContextMap()
MDC.setContextMap(Map) 为当前线程设置 MDC 上下文 MDC.setContextMap(context)
MDC.clear() 清除当前线程的 MDC 上下文 MDC.clear()

二、基础实现:Web 请求 TraceId 链路追踪

先实现最基础的 Web 请求链路追踪,让每个 HTTP 请求的所有同步日志都携带 TraceId。

1. 依赖准备(SpringBoot 2.x/3.x 通用)

无需额外依赖,SpringBoot 自带的 spring-boot-starter-web 和 spring-boot-starter-logging 已满足需求,若使用 Lombok 可简化日志声明:

<dependencies>
    <!-- SpringBoot Web 核心依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 日志依赖(默认集成 Logback) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
    <!-- Lombok 简化日志 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 日志配置:自动打印 TraceId

修改 Logback 配置文件(src/main/resources/logback-spring.xml),在日志格式中添加 %X{traceId}(MDC 中 TraceId 的 key):

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!-- 日志存储路径 -->
    <property name="LOG_PATH" value="${user.home}/logs/trace-demo" />
    <!-- 日志输出格式(核心:添加 [traceId:%X{traceId:-NONE}]) -->
    <property name="LOG_PATTERN" value="[traceId:%X{traceId:-NONE}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n" />

    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <!-- 按天滚动文件输出 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>${LOG_PATH}/app-%d{yyyy-MM-dd}.log</FileNamePattern>
            <MaxHistory>30</MaxHistory> <!-- 保留 30 天日志 -->
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize> <!-- 单个日志文件最大 10MB -->
        </triggeringPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
</configuration>
  • %X{traceId:-NONE} 表示:若 MDC 中存在 traceId 则打印,否则打印 NONE;
  • 配置文件路径需在 application.yml 中指定(可选,SpringBoot 会自动扫描 classpath:logback-spring.xml):

    logging:
    config: classpath:logback-spring.xml
    server:
    port: 8080

3. 自定义拦截器:生成并传递 TraceId

通过 SpringMVC 拦截器,在请求入口生成 TraceId 并存入 MDC,请求结束后清除:

import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * Web 请求 TraceId 拦截器
 */
@Slf4j
public class TraceIdInterceptor implements HandlerInterceptor {
    // TraceId 的 key(需与日志配置中的 %X{traceId} 一致)
    public static final String TRACE_ID_KEY = "traceId";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 1. 优先从请求头获取 TraceId(支持前端/网关传递,便于跨服务追踪)
        String traceId = request.getHeader(TRACE_ID_KEY);
        // 2. 若请求头未携带,生成唯一 TraceId(UUID 去除横线,缩短长度)
        if (!StringUtils.hasText(traceId)) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }
        // 3. 存入 MDC
        MDC.put(TRACE_ID_KEY, traceId);
        // 4. 可选:将 TraceId 写入响应头,便于前端排查
        response.setHeader(TRACE_ID_KEY, traceId);
        log.info("请求入口:URI={}, Method={}, TraceId={}", 
                 request.getRequestURI(), request.getMethod(), traceId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, 
                               @Nullable Exception ex) {
        // 5. 请求结束后清除 MDC,避免 ThreadLocal 内存泄漏(Tomcat 线程池复用)
        log.info("请求结束:URI={}, TraceId={}", request.getRequestURI(), MDC.get(TRACE_ID_KEY));
        MDC.clear();
    }
}

4. 注册拦截器

通过 WebMvcConfigurer 注册拦截器,确保拦截所有 Web 请求:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

/**
 * SpringMVC 配置
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Resource
    private TraceIdInterceptor traceIdInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(traceIdInterceptor)
                .addPathPatterns("/**") // 拦截所有请求
                .excludePathPatterns("/error", "/health"); // 排除健康检查、错误页等无需追踪的路径
    }
}

5. 测试:同步请求链路追踪

编写测试接口和服务,验证 TraceId 传递:

// 测试控制器
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {

    @Resource
    private TestService testService;

    @GetMapping("/sync")
    public String syncTest(String name) {
        log.info("控制器接收请求:name={}", name);
        String result = testService.process(name);
        log.info("控制器返回结果:result={}", result);
        return result;
    }
}

// 测试服务
@Service
@Slf4j
public class TestService {

    public String process(String name) {
        log.info("服务层处理:name={}", name);
        String data = queryData(name);
        log.info("服务层处理完成:data={}", data);
        return "Hello, " + data;
    }

    private String queryData(String name) {
        log.info("数据查询:name={}", name);
        // 模拟数据库查询
        return name + "_" + System.currentTimeMillis();
    }
}

测试结果(日志片段):

[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.123 [http-nio-8080-exec-1] INFO  c.example.trace.controller.TestController - 请求入口:URI=/test/sync, Method=GET, TraceId=672e16b742f54119b28a6112bd21ccb0
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.124 [http-nio-8080-exec-1] INFO  c.example.trace.controller.TestController - 控制器接收请求:name=zhangsan
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.125 [http-nio-8080-exec-1] INFO  c.example.trace.service.TestService - 服务层处理:name=zhangsan
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.126 [http-nio-8080-exec-1] INFO  c.example.trace.service.TestService - 数据查询:name=zhangsan
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.127 [http-nio-8080-exec-1] INFO  c.example.trace.service.TestService - 服务层处理完成:data=zhangsan_1731329400126
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.128 [http-nio-8080-exec-1] INFO  c.example.trace.controller.TestController - 控制器返回结果:result=Hello, zhangsan_1731329400126
[traceId:672e16b742f54119b28a6112bd21ccb0] 2024-11-11 15:30:00.129 [http-nio-8080-exec-1] INFO  c.example.trace.interceptor.TraceIdInterceptor - 请求结束:URI=/test/sync, TraceId=672e16b742f54119b28a6112bd21ccb0

所有日志通过同一个 TraceId 串联,清晰可见请求的完整链路。

三、进阶场景 1:异步线程 TraceId 传递

同步请求的 TraceId 依赖 ThreadLocal 自动传递,但异步线程(线程池、@Async)会切换线程,导致 MDC 中的 TraceId 丢失——需手动将父线程的 MDC 上下文传递给子线程。

1. 核心工具类:封装线程任务,传递 MDC 上下文

import org.slf4j.MDC;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;

/**
 * MDC 线程上下文传递工具类
 */
public final class MdcThreadUtil {
    private static final String TRACE_ID_KEY = TraceIdInterceptor.TRACE_ID_KEY;

    /**
     * 生成 TraceId
     */
    public static String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    /**
     * 为线程池任务包装 MDC 上下文(Runnable 无返回值)
     */
    public static Runnable wrap(Runnable task) {
        // 复制父线程的 MDC 上下文
        Map<String, String> parentContext = MDC.getCopyOfContextMap();
        return () -> {
            try {
                // 子线程设置 MDC 上下文
                if (parentContext != null) {
                    MDC.setContextMap(parentContext);
                } else {
                    // 若父线程无上下文,生成默认 TraceId
                    MDC.put(TRACE_ID_KEY, generateTraceId());
                }
                task.run(); // 执行任务
            } finally {
                // 清除子线程 MDC,避免内存泄漏
                MDC.clear();
            }
        };
    }

    /**
     * 为线程池任务包装 MDC 上下文(Callable 有返回值)
     */
    public static <T> Callable<T> wrap(Callable<T> task) {
        Map<String, String> parentContext = MDC.getCopyOfContextMap();
        return () -> {
            try {
                if (parentContext != null) {
                    MDC.setContextMap(parentContext);
                } else {
                    MDC.put(TRACE_ID_KEY, generateTraceId());
                }
                return task.call();
            } finally {
                MDC.clear();
            }
        };
    }
}

2. 自定义线程池:自动传递 MDC 上下文

重写 Spring 线程池,对提交的任务自动进行 MDC 包装:

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Callable;
import java.util.concurrent.Future;

/**
 * 支持 MDC 上下文传递的线程池
 */
public class MdcThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

    @Override
    public void execute(Runnable task) {
        // 包装任务,传递 MDC 上下文
        super.execute(MdcThreadUtil.wrap(task));
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(MdcThreadUtil.wrap(task));
    }

    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(MdcThreadUtil.wrap(task));
    }
}

3. 配置线程池 Bean

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

import java.util.concurrent.Executor;

/**
 * 异步线程池配置
 */
@EnableAsync
@Configuration
public class AsyncConfig {

    @Bean("asyncExecutor")
    public Executor asyncExecutor() {
        MdcThreadPoolTaskExecutor executor = new MdcThreadPoolTaskExecutor();
        executor.setCorePoolSize(5); // 核心线程数
        executor.setMaxPoolSize(10); // 最大线程数
        executor.setQueueCapacity(100); // 队列容量
        executor.setKeepAliveSeconds(60); // 空闲线程存活时间
        executor.setThreadNamePrefix("async-"); // 线程名前缀
        executor.initialize();
        return executor;
    }
}

4. 测试:异步线程 TraceId 传递

@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {

    @Resource
    private TestService testService;

    @GetMapping("/async")
    public String asyncTest(String name) {
        log.info("控制器接收请求:name={}", name);
        // 调用异步方法
        testService.asyncProcess(name);
        log.info("控制器返回(异步任务已提交)");
        return "Async task submitted";
    }
}

@Service
@Slf4j
public class TestService {

    @Async("asyncExecutor") // 指定自定义线程池
    public void asyncProcess(String name) {
        log.info("异步服务层处理:name={}", name);
        try {
            // 模拟耗时操作
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            log.error("异步处理异常", e);
            Thread.currentThread().interrupt();
        }
        log.info("异步服务层处理完成:name={}", name);
    }
}

测试结果(日志片段):

[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:00.456 [http-nio-8080-exec-2] INFO  c.example.trace.controller.TestController - 请求入口:URI=/test/async, Method=GET, TraceId=89a3f2d17c4e43b88e9d3578a9123456
[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:00.457 [http-nio-8080-exec-2] INFO  c.example.trace.controller.TestController - 控制器接收请求:name=lisi
[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:00.458 [http-nio-8080-exec-2] INFO  c.example.trace.controller.TestController - 控制器返回(异步任务已提交)
[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:00.459 [async-1] INFO  c.example.trace.service.TestService - 异步服务层处理:name=lisi
[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:02.460 [async-1] INFO  c.example.trace.service.TestService - 异步服务层处理完成:name=lisi
[traceId:89a3f2d17c4e43b88e9d3578a9123456] 2024-11-11 15:35:02.461 [http-nio-8080-exec-2] INFO  c.example.trace.interceptor.TraceIdInterceptor - 请求结束:URI=/test/async, TraceId=89a3f2d17c4e43b88e9d3578a9123456

异步线程的日志与主线程的 TraceId 一致,链路完整贯通。

四、进阶场景 2:定时任务 TraceId 追踪

定时任务(@Scheduled)不经过 Web 拦截器,因此需要通过 AOP 为其生成 TraceId,确保定时任务的日志也能被追踪。

1. 定时任务线程池配置

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executors;

/**
 * 定时任务线程池配置
 */
@EnableScheduling
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        // 配置定时任务线程池(5个线程)
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
    }
}

2. AOP 切面:为定时任务生成 TraceId

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 定时任务 TraceId AOP 切面
 */
@Aspect
@Configuration
public class SchedulerTraceAspect {
    private static final Logger log = LoggerFactory.getLogger(SchedulerTraceAspect.class);
    private static final String TRACE_ID_KEY = TraceIdInterceptor.TRACE_ID_KEY;

    // 切点:所有标注 @Scheduled 的方法
    @Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled)")
    public void schedulerPointcut() {}

    @Around("schedulerPointcut()")
    public Object aroundScheduler(ProceedingJoinPoint joinPoint) throws Throwable {
        String traceId = null;
        try {
            // 生成 TraceId
            traceId = MdcThreadUtil.generateTraceId();
            MDC.put(TRACE_ID_KEY, traceId);
            log.info("定时任务开始:task={}, TraceId={}", 
                     joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(),
                     traceId);
            // 执行定时任务
            return joinPoint.proceed();
        } finally {
            // 清除 MDC
            log.info("定时任务结束:task={}, TraceId={}",
                     joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(),
                     traceId);
            MDC.clear();
        }
    }
}

3. 测试:定时任务 TraceId

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
 * 测试定时任务
 */
@Service
public class TestScheduler {
    private static final Logger log = LoggerFactory.getLogger(TestScheduler.class);

    // 每1分钟执行一次
    @Scheduled(cron = "0 0/1 * * * ?")
    public void testTask() {
        log.info("执行定时任务,当前时间:{}", new Date());
        // 模拟业务逻辑
        doBusiness();
    }

    private void doBusiness() {
        log.info("定时任务业务处理中...");
    }
}

测试结果(日志片段):

[traceId:2d4f7a9c3b6e418d9c7a1234567890ab] 2024-11-11 15:40:00.001 [pool-2-thread-1] INFO  c.example.trace.aspect.SchedulerTraceAspect - 定时任务开始:task=com.example.trace.scheduler.TestScheduler.testTask, TraceId=2d4f7a9c3b6e418d9c7a1234567890ab
[traceId:2d4f7a9c3b6e418d9c7a1234567890ab] 2024-11-11 15:40:00.002 [pool-2-thread-1] INFO  c.example.trace.scheduler.TestScheduler - 执行定时任务,当前时间:Mon Nov 11 15:40:00 CST 2024
[traceId:2d4f7a9c3b6e418d9c7a1234567890ab] 2024-11-11 15:40:00.003 [pool-2-thread-1] INFO  c.example.trace.scheduler.TestScheduler - 定时任务业务处理中...
[traceId:2d4f7a9c3b6e418d9c7a1234567890ab] 2024-11-11 15:40:00.004 [pool-2-thread-1] INFO  c.example.trace.aspect.SchedulerTraceAspect - 定时任务结束:task=com.example.trace.scheduler.TestScheduler.testTask, TraceId=2d4f7a9c3b6e418d9c7a1234567890ab

定时任务的所有日志通过独立的 TraceId 串联,便于排查定时任务相关问题。

五、高阶场景:微服务间 TraceId 传递

在微服务架构中,请求会跨多个服务(如服务 A → 服务 B → 服务 C),需要将 TraceId 通过 HTTP 请求头传递,实现全链路贯通。

1. 微服务调用工具:RestTemplate 传递 TraceId

通过拦截器为 RestTemplate 添加 TraceId 请求头:

import org.slf4j.MDC;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

/**
 * RestTemplate 配置(微服务调用传递 TraceId)
 */
@Configuration
public class RestTemplateConfig {

    private static final String TRACE_ID_KEY = TraceIdInterceptor.TRACE_ID_KEY;

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        // 添加拦截器,传递 TraceId
        List<ClientHttpRequestInterceptor> interceptors = Collections.singletonList(
            new ClientHttpRequestInterceptor() {
                @Override
                public ClientHttpResponse intercept(HttpRequest request, byte[] body, 
                                                 ClientHttpRequestExecution execution) throws IOException {
                    // 从 MDC 获取 TraceId,添加到请求头
                    String traceId = MDC.get(TRACE_ID_KEY);
                    if (traceId != null) {
                        HttpHeaders headers = request.getHeaders();
                        headers.add(TRACE_ID_KEY, traceId);
                    }
                    return execution.execute(request, body);
                }
            }
        );
        restTemplate.setInterceptors(interceptors);
        return restTemplate;
    }
}

2. 微服务接收端:复用 Web 拦截器

微服务 B/C 只需复用前面实现的 TraceIdInterceptor,即可从请求头中获取 TraceId 并存入 MDC——无需额外开发,实现跨服务 TraceId 传递。

3. 测试:微服务间 TraceId 贯通

  • 服务 A 调用服务 B:

    @RestController
    @Slf4j
    public class ServiceAController {
    
      @Resource
      private RestTemplate restTemplate;
    
      @GetMapping("/call-service-b")
      public String callServiceB() {
          log.info("服务 A 准备调用服务 B");
          // 调用服务 B(TraceId 通过请求头传递)
          String result = restTemplate.getForObject("http://service-b/test/echo", String.class);
          log.info("服务 A 调用服务 B 完成,结果:{}", result);
          return result;
      }
    }
  • 服务 B 接收并响应:

    @RestController
    @Slf4j
    public class ServiceBController {
    
      @GetMapping("/test/echo")
      public String echo() {
          log.info("服务 B 接收请求,TraceId={}", MDC.get(TraceIdInterceptor.TRACE_ID_KEY));
          return "Hello from Service B";
      }
    }

测试结果(日志片段):

  • 服务 A 日志:

    [traceId:4e7a9d3b6f8e429c8d7b1234567890cd] 2024-11-11 15:45:00.789 [http-nio-8080-exec-3] INFO  c.example.trace.controller.ServiceAController - 服务 A 准备调用服务 B
    [traceId:4e7a9d3b6f8e429c8d7b1234567890cd] 2024-11-11 15:45:00.901 [http-nio-8080-exec-3] INFO  c.example.trace.controller.ServiceAController - 服务 A 调用服务 B 完成,结果:Hello from Service B
  • 服务 B 日志:

    [traceId:4e7a9d3b6f8e429c8d7b1234567890cd] 2024-11-11 15:45:00.850 [http-nio-8081-exec-1] INFO  c.example.trace.controller.ServiceBController - 服务 B 接收请求,TraceId=4e7a9d3b6f8e429c8d7b1234567890cd

    跨服务的 TraceId 完全一致,实现全链路追踪。

六、生产环境优化与避坑指南

1. 内存泄漏防护

  • 必须在请求结束/线程执行完成后调用 MDC.clear():Tomcat 线程池、自定义线程池会复用线程,未清除的 MDC 会导致 TraceId 串流(不同请求共用同一个 TraceId);
  • 异步线程的 MDC 清除必须放在 finally 块中:即使任务执行异常,也能确保 MDC 被清除。

2. TraceId 生成优化

  • 避免使用过长的 TraceId:UUID 去除横线后长度为 32 位,足够唯一且不占用过多日志空间;
  • 支持前端传递 TraceId:便于前端排查问题(如用户反馈问题时,前端传递 TraceId 给后端,直接定位日志)。

3. 日志检索优化

  • 日志文件按 TraceId 相关维度滚动:如按天滚动 + 按服务名区分文件,便于日志检索工具(ELK、SkyWalking)快速查询;
  • 集成日志检索工具:将日志导入 ELK,通过 TraceId 快速筛选出全链路日志,排查问题效率翻倍。

4. 特殊场景处理

  • 异常链路:全局异常处理器中需保留 TraceId,在异常日志中打印,便于定位问题根源;
  • 第三方服务调用:调用第三方服务(如支付、短信)时,将 TraceId 作为请求参数或请求头传递,便于第三方反馈问题时关联日志;
  • 消息队列:发送消息时,将 TraceId 存入消息头;消费消息时,从消息头中提取 TraceId 存入 MDC,打通“服务 → 消息队列 → 消费服务”的链路。

七、总结:TraceId 链路追踪的核心价值

TraceId 链路追踪看似简单,却能解决复杂系统排查问题的“痛点”——它将分散的日志串联成完整链路,让问题定位从“数小时”缩短到“分钟级”。本文实现的方案覆盖:

  • 基础场景:Web 同步请求;
  • 进阶场景:异步线程、定时任务;
  • 高阶场景:微服务跨服务调用;
  • 生产级保障:内存泄漏防护、日志检索优化。

在实际落地时,可根据业务复杂度灵活扩展(如添加 SpanId 支持更细粒度的链路追踪、集成 SkyWalking 等分布式追踪工具)。记住:日志链路追踪不是“银弹”,但它是排查问题的“必备工具”,能让你在复杂系统中快速找到问题根源,大幅提升运维效率。

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/hou-duan/4559

相关文章

  • 深度解析多级缓存架构:从设计到落地,彻底解决数据一致性难题
  • Spring事件驱动深度指南:从单机异步到亿级流量,比MQ更轻的架构神器
  • 重构 Controller 终极指南:从臃肿到优雅的 7 大黄金法则 + 实战技巧
  • 从3秒到30毫秒!SpringBoot树形结构深度优化指南:不止于O(n)算法的全链路提速方案
  • MyBatis vs Spring Data JPA 从原理到实战全解析
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: MDC SpringBoot TraceId 链路追踪
最后更新:2025年11月5日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >

文章评论

1 2 3 4 5 6 7 8 9 11 12 13 14 15 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 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
取消回复

位卑未敢忘忧国,事定犹须待阖棺。

那年今日(12月17日)

  • 1981年:德国足球运动员蒂姆·维泽出生
  • 1971年:印度和东巴基斯坦达成停火协议
  • 1909年:比利时国王利奥波德二世逝世
  • 1905年:狙击之王西蒙·海耶出生
  • 1902年:京师大学堂正式开学
  • 更多历史事件
最新 热点 随机
最新 热点 随机
AI原生数据库新标杆:seekdb深度解析,轻量架构与混合搜索的双重革命 做了一个WordPress文章热力图插件 Spring WebFlux底层原理深度剖析-从响应式流到事件循环的全链路拆解 Spring WebFlux深度解析:异步非阻塞架构与实战落地指南 规范驱动AI编程:用OpenSpec实现100%可控开发,从需求到代码的全流程闭环 WordPress网站换了个字体,差点儿把样式换崩了
玩博客的人是不是越来越少了?准备入手个亚太的ECS,友友们有什么建议吗?使用WireGuard在Ubuntu 24.04系统搭建VPNWordPress实现用户评论等级排行榜插件Gemini 3 Pro 深度测评:多模态AI编程的跨代际突破,从一句话到完整应用的全链路革命WordPress网站换了个字体,差点儿把样式换崩了
使用itext和freemarker来根据Html模板生成PDF文件,加水印、印章 项目中不用 redis 分布式锁,怎么防止用户重复提交? SpringBoot框架自动配置之spring.factories和AutoConfiguration.imports JAVA线程池简析(JDK1.6) IDEA版本2020.*全局MAVEN配置 Gemini 3 深度解析:从像素级复刻到 AGI 雏形,多模态 AI 如何重构开发与创作?
标签聚合
JVM WordPress SQL 日常 K8s 架构 SpringBoot AI编程 MySQL ElasticSearch 多线程 分布式 数据库 AI JAVA docker 设计模式 Spring IDEA Redis
友情链接
  • Blogs·CN
  • Honesty
  • 临窗旋墨
  • 哥斯拉
  • 彬红茶日记
  • 志文工作室
  • 搬砖日记
  • 旧时繁华
  • 林羽凡
  • 瓦匠个人小站
  • 皮皮社
  • 知向前端
  • 蜗牛工作室
  • 韩小韩博客
  • 风渡言

COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

Theme Kratos Made By Dylan

津ICP备2024022503号-3