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>
相关推荐
AskHarries5 分钟前
不用公网 IP,把 Windows 和 Linux 服务器放进同一个局域网:Tailscale 组网实战
后端
神奇小汤圆6 分钟前
Java 的1 亿次对象创建:JVM 开启 / 关闭逃逸分析,GC 性能差距巨大
后端
tangdou3690986559 分钟前
AI真好玩系列-2分钟快速了解DeepAgents | Quick Guide to DeepAgents in 2 Minutes
前端·javascript·后端
神奇小汤圆25 分钟前
面试官:MySQL 为什么要是使用 MVCC?原理是什么?
后端
像我这样帅的人丶你还33 分钟前
Java 后端详解(五):Redis 缓存
java·后端·全栈
杉氧37 分钟前
深度解析:Jetpack Compose 核心架构与底层原理 —— 十年安卓老兵的“破茧重生”
android·架构·android jetpack
玉宇夕落1 小时前
别再死磕 Prompt 了!上下文工程 (Context Engineering) 的简单学习
后端
Lion091 小时前
ReAct 循环:Agent 的思考引擎 — Think → Act → Observe
架构
用户34232323763171 小时前
定时器与 PWM 输出详解
后端
Jason_chen2 小时前
Linux 6.2 CAN/CANFD机制详解
后端