Java 项目中实现统一的 追踪ID,traceId实现分布式系统追踪

在 Java 项目中实现分布式追踪系统时,traceId 的传递机制可以通过以下图示来说明:

  1. 分布式追踪系统架构图

    [用户请求]


    [API网关] → 生成全局 traceId


    [服务A] → 接收 traceId → 日志记录 → 调用服务B
    │ ▲
    │ │ 传递 traceId
    ▼ │
    [服务B] → 接收 traceId → 日志记录 → 调用数据库
    │ ▲
    │ │ 传递 traceId
    ▼ │
    [数据库] → 记录 traceId 到 SQL 日志


    [消息队列] → 传递 traceId


    [服务C] → 处理消息并记录 traceId

  2. traceId 传递机制图解

2.1 HTTP 请求中的传递

复制代码
[客户端] → GET /api/data
          Header: X-Trace-Id: abc123
          │
          ▼
[服务A] → 从 Header 获取 traceId
          │
          ▼
[服务A] → 调用服务B: POST /internal/data
          Header: X-Trace-Id: abc123 (保持相同)

2.2 RPC 调用中的传递

复制代码
[服务A] → Dubbo RPC 调用服务B
   ┌───────────────────────────┐
   │ Invocation                │
   │   - method: getData       │
   │   - attachments:          │
   │        traceId = "abc123" │
   └───────────────────────────┘
          │
          ▼
[服务B] → 从 attachments 获取 traceId

2.3 消息队列中的传递

服务A → 发送消息到 RabbitMQ/Kafka

┌───────────────────────────┐

│ Message Properties │

│ headers: │

│ traceId = "abc123" │

└───────────────────────────┘

服务C → 从消息属性获取 traceId

  1. 线程上下文管理

3.1 同步请求中的上下文

┌──────────────────────┐

│ ThreadLocal │

│ traceId = "abc123" │

│ MDC (日志上下文) │

└──────────────────────┘

3.2 异步线程中的上下文传递

主线程 → 提交任务到线程池

┌──────────────────────┐ ┌──────────────────────┐

│ TransmittableThread │ → │ 线程池线程 │

│ traceId = "abc123" │ │ traceId = "abc123" │

└──────────────────────┘ └──────────────────────┘

任务执行前复制 任务执行时使用

  1. 日志系统中的 traceId 集成

2023-08-15 10:30:25 http-nio-8080-exec-1 traceId:abc123 INFO c.e.s.ServiceA - 处理请求

2023-08-15 10:30:26 http-nio-8080-exec-1 traceId:abc123 DEBUG c.e.s.ServiceA - 调用服务B

2023-08-15 10:30:27 http-nio-8080-exec-3 traceId:abc123 INFO c.e.s.ServiceB - 接收请求

2023-08-15 10:30:28 task-pool-1 traceId:abc123 INFO c.e.s.AsyncService - 异步任务开始

  1. 全链路追踪示意图

┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐

│ 网关 │ │ 服务A │ │ 服务B │ │ 数据库 │

│ traceId生成 │ ⇒⇒⇒ │ 记录日志 │ ⇒⇒⇒ │ 记录日志 │ ⇒⇒⇒ │ SQL日志 │

│ abc123 │ │ abc123 │ │ abc123 │ │ abc123

└────────────┘ └────────────┘ └──────┬──────┘ └──────────┘

⇓ ⇓

┌────────────┐ │ ┌────────────┐

│ 消息队列 │⇐═⇘ │ 服务C │

abc123 │ │ abc123

└────────────┘ └────────────┘

关键组件说明:

  1. traceId 生成器:

◦ 在系统入口(网关或第一个服务)生成唯一 ID

◦ 格式:应用标识-时间戳-随机数(例:APP1-1692081025-7d3f)

  1. 上下文传播器:

◦ HTTP:通过 Header(X-Trace-Id)

◦ RPC:通过调用附件(Attachment)

◦ MQ:通过消息属性(Properties)

◦ 线程:通过 ThreadLocal + MDC

  1. 日志集成:

◦ 日志框架配置中添加 %X{traceId}

◦ 所有日志自动包含当前 traceId

  1. 异步支持:

◦ 使用 TransmittableThreadLocal 解决线程池上下文传递

◦ 配合 TaskDecorator 包装 Runnable

  1. 监控集成:

◦ 将 traceId 传递给监控系统(如 Zipkin)

◦ 实现全链路追踪可视化

这种设计确保了在分布式系统中,从一个请求进入系统开始,到经过多个服务、数据库操作、异步任务处理,再到最终响应,整个过程中的所有日志和操作都能通过同一个 traceId 关联起来,极大地方便了问题排查和系统监控。

在 Java 项目中实现统一的 traceId(请求追踪 ID)是分布式系统追踪的关键,以下是几种常用且可靠的实现方案:


方案一:Servlet Filter + MDC(推荐)

复制代码
// TraceId 工具类
public class TraceIdUtil {
    public static final String TRACE_ID = "traceId";
    
    public static String generateTraceId() {
        // 使用 UUID + 时间戳确保唯一性
        return "TRC-" + System.currentTimeMillis() + "-" + UUID.randomUUID().toString().substring(0, 8);
    }
}

或者:
import java.util.UUID;

public class TraceIdGenerator {
    
    // 生成标准UUID格式的TraceId
    public static String generateUUIDTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }
    
    // 生成更短的UUID变种(32位)
    public static String generateShortUUIDTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }
    
    // 生成带前缀的TraceId
    public static String generatePrefixedTraceId(String prefix) {
        return prefix + "-" + generateShortUUIDTraceId();
    }
}



// 过滤器实现
@WebFilter("/*")
public class TraceIdFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
        throws IOException, ServletException {
        
        try {
            // 1. 生成/传递 traceId
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            String traceId = Optional.ofNullable(httpRequest.getHeader("X-Trace-Id"))
                                    .orElseGet(TraceIdUtil::generateTraceId);
            
            // 2. 存入 MDC(日志上下文)
            MDC.put(TraceIdUtil.TRACE_ID, traceId);
            
            // 3. 设置到请求属性(供业务代码使用)
            httpRequest.setAttribute(TRACE_ID_ATTRIBUTE, traceId);
            
            // 4. 传递给下游(设置响应头)
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("X-Trace-Id", traceId);
            
            chain.doFilter(request, response);
        } finally {
            // 5. 请求结束后清除 MDC
            MDC.clear();
        }
    }
}

日志配置(logback.xml)

复制代码
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <!-- 添加 %X{traceId} 输出 -->
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

方案二:Spring Interceptor + ThreadLocal

复制代码
// ThreadLocal 上下文
public class TraceContext {
    private static final ThreadLocal<String> traceIdHolder = new ThreadLocal<>();
    
    public static void setTraceId(String traceId) {
        traceIdHolder.set(traceId);
    }
    
    public static String getTraceId() {
        return traceIdHolder.get();
    }
    
    public static void clear() {
        traceIdHolder.remove();
    }
}

// 拦截器实现
@Component
public class TraceInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String traceId = request.getHeader("X-Trace-Id");
        if (traceId == null) {
            traceId = TraceIdUtil.generateTraceId();
        }
        
        TraceContext.setTraceId(traceId);
        response.setHeader("X-Trace-Id", traceId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                Object handler, Exception ex) {
        TraceContext.clear(); // 清除ThreadLocal
    }
}

注册拦截器:

复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired private TraceInterceptor traceInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(traceInterceptor);
    }
}

方案三:Spring Cloud Sleuth(微服务专用)

复制代码
<!-- pom.xml 添加依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

自动实现

  • 自动生成 traceId(格式:application-traceId-spanId
  • 通过 HTTP Headers 传递(X-B3-TraceId, X-B3-SpanId
  • 集成日志框架(MDC 自动注入)
  • 支持 OpenZipkin 分布式追踪

方案四:异步线程支持(关键扩展)

使用 TransmittableThreadLocal 解决线程池 traceId 传递问题:

复制代码
// 增强版 ThreadLocal
public class AsyncTraceContext {
    private static final TransmittableThreadLocal<String> traceIdHolder = 
        new TransmittableThreadLocal<>();
    
    // 方法同方案二...
}

// 线程池包装器
@Bean
public Executor asyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setTaskDecorator(runnable -> 
        TtlRunnable.get(() -> {
            // 传递 traceId 到子线程
            AsyncTraceContext.setTraceId(TraceContext.getTraceId());
            runnable.run();
        })
    );
    return executor;
}

方案五:RPC 调用传递

1. HTTP 客户端(RestTemplate)
复制代码
@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.getInterceptors().add((request, body, execution) -> {
        // 将当前 traceId 设置到请求头
        request.getHeaders().add("X-Trace-Id", TraceContext.getTraceId());
        return execution.execute(request, body);
    });
    return restTemplate;
}
2. Dubbo 调用
复制代码
// 消费者过滤器
public class DubboTraceConsumerFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) {
        invocation.setAttachment("traceId", TraceContext.getTraceId());
        return invoker.invoke(invocation);
    }
}

// 提供者过滤器
public class DubboTraceProviderFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) {
        String traceId = invocation.getAttachment("traceId");
        if (traceId != null) {
            TraceContext.setTraceId(traceId);
        }
        return invoker.invoke(invocation);
    }
}

最佳实践总结

  1. 入口生成:在请求入口(Filter/Interceptor)生成 traceId
  2. 全链路传递
    • HTTP:通过 Header 传递(X-Trace-Id
    • RPC:通过调用附件传递
    • MQ:通过消息头传递
  1. 存储位置
    • 同步请求:使用 MDC + ThreadLocal
    • 异步线程:使用 TransmittableThreadLocal
  1. 日志集成 :在日志模板中添加 %X{traceId}

  2. 异常处理:确保在 finally 块中清理上下文

  3. ID 生成规则

    // 示例:服务前缀 + 时间戳 + 随机数
    "SVC-" + System.currentTimeMillis() + "-" + ThreadLocalRandom.current().nextInt(10000)


验证方式

  1. 查看日志输出是否包含 traceId

  2. 跨服务调用检查 Header 传递

  3. 异步任务验证 traceId 一致性

    @Async
    public void asyncTask() {
    log.info("Async task traceId: {}", MDC.get("traceId")); // 应非空
    }

选择方案时:

  • 单体应用 → 方案一
  • Spring MVC → 方案二
  • 微服务架构 → 方案三
  • 复杂异步场景 → 方案四

traceId实践:

复制代码
生成traceId

import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

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

/**
 */
@Component
@Slf4j
public class LogInterceptor extends HandlerInterceptorAdapter {

    private final static   ThreadLocal<Long> timeThreadLocal = new ThreadLocal<>();
    public final  static TransmittableThreadLocal<String> requestIdThreadLocal = new TransmittableThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        timeThreadLocal.set(System.currentTimeMillis());
        // 将当前 traceId 放入请求头
        String traceId = request.getHeader("X-Trace-Id");
        if (traceId == null) {
            traceId = UUID.randomUUID().toString();
        }
        request.setAttribute("performanceRequestId", traceId);
        requestIdThreadLocal.set(traceId);
        String path = request.getServletPath();
        log.info("==> 调用开始 访问服务uri:{}", path);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            long startTime = timeThreadLocal.get();
            long endTime = System.currentTimeMillis();
            log.info("==> 调用结束 uri:{} 耗时 {} ms", request.getServletPath(), endTime - startTime);
        }finally {
            timeThreadLocal.remove();
            requestIdThreadLocal.remove();
        }
    }

}

对应转换类:

import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;

/**
 */
public class MyLogBackConverter extends ClassicConverter {


    @Override
    public String convert(ILoggingEvent event) {
        // 获取你的参数,例如从 MDC 或者其他上下文中
//        String myParam = event.getMDCPropertyMap().get("myParam");
        String myParam = LogInterceptor.requestIdThreadLocal.get();
        return myParam != null ? myParam : "TraceID";
    }
}

对应logback:

复制代码
<configuration>
    <property name="LOG_HOME" value="logs/xxxx-service" />

    <conversionRule conversionWord="myParam" converterClass="com.xxx.config.MyLogBackConverter" />

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!-- 只接受INFO级别的日志 -->
            <level>INFO</level>
        </filter>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%myParam] %-5level %logger{36} - %msg %n</pattern>
        </encoder>
    </appender>
    <appender name="STDOUT_ERROR" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!-- 只接受INFO级别的日志 -->
            <level>ERROR</level>
        </filter>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%myParam] %-5level %logger{36} - %msg %n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="STDOUT_ERROR" />
    </root>

</configuration>
相关推荐
xieliyu.7 小时前
Java算法精讲:双指针(三)
java·开发语言·算法
love530love7 小时前
LiveTalking 数字人项目 Windows 部署完全指南(EPGF 架构)
人工智能·windows·python·架构·livetalking·epgf
星辰徐哥7 小时前
Spring Boot 微服务架构设计与实现
spring boot·后端·微服务
星辰徐哥7 小时前
Spring Boot 数据导入导出与报表生成
spring boot·后端·ui
明夜之约7 小时前
Spring Boot 自动装配源码
java·spring boot·后端
Leaton Lee7 小时前
Spring Boot分层架构详解:从Controller到Service再到Mapper的完整流程
java·spring boot·后端·架构
Micro麦可乐7 小时前
Spring Boot 实战:从零设计一个短链系统(含完整代码与数据库设计)
数据库·spring boot·后端·哈希算法·雪花算法·短链系统
Jinkxs7 小时前
Resilience4j- 与 Spring Boot 快速集成:自动配置与基础注解使用
java·spring boot·后端
毕设源码_郑学姐7 小时前
计算机毕业设计springboot网络相册设计与实现 基于Spring Boot框架的在线相册管理系统开发与应用 Spring Boot驱动的网络影集设计与实践
spring boot·后端·课程设计
辣机小司7 小时前
【踩坑记录:Spring Boot 配置文件读取值不一致?警惕 YAML 的“八进制陷阱”与 SnakeYAML 版本之谜】
java·spring boot·后端·yaml·踩坑记录