在 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
3. 线程上下文管理
3.1 同步请求中的上下文
┌──────────────────────┐
│ ThreadLocal │
│ traceId = "abc123" │
│ MDC (日志上下文) │
└──────────────────────┘
3.2 异步线程中的上下文传递
主线程 → 提交任务到线程池
┌──────────────────────┐ ┌──────────────────────┐
│ TransmittableThread │ → │ 线程池线程 │
│ traceId = "abc123" │ │ traceId = "abc123" │
└──────────────────────┘ └──────────────────────┘
任务执行前复制 任务执行时使用
4. 日志系统中的 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 - 异步任务开始
5. 全链路追踪示意图
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐
│ 网关 │ │ 服务A │ │ 服务B │ │ 数据库 │
│ traceId生成 │ ⇒⇒⇒ │ 记录日志 │ ⇒⇒⇒ │ 记录日志 │ ⇒⇒⇒ │ SQL日志 │
│ abc123 │ │ [abc123] │ │ [abc123] │ │ [abc123] │
└────────────┘ └────────────┘ └──────┬──────┘ └──────────┘
⇓ ⇓
┌────────────┐ │ ┌────────────┐
│ 消息队列 │⇐═⇘ │ 服务C │
│ [abc123] │ │ [abc123] │
└────────────┘ └────────────┘
关键组件说明:
1. traceId 生成器:
◦ 在系统入口(网关或第一个服务)生成唯一 ID
◦ 格式:[应用标识]-[时间戳]-[随机数](例:APP1-1692081025-7d3f)
2. 上下文传播器:
◦ HTTP:通过 Header(X-Trace-Id)
◦ RPC:通过调用附件(Attachment)
◦ MQ:通过消息属性(Properties)
◦ 线程:通过 ThreadLocal + MDC
3. 日志集成:
◦ 日志框架配置中添加 %X{traceId}
◦ 所有日志自动包含当前 traceId
4. 异步支持:
◦ 使用 TransmittableThreadLocal 解决线程池上下文传递
◦ 配合 TaskDecorator 包装 Runnable
5. 监控集成:
◦ 将 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);
}
}
最佳实践总结
- 入口生成:在请求入口(Filter/Interceptor)生成 traceId
- 全链路传递:
-
- HTTP:通过 Header 传递(
X-Trace-Id
) - RPC:通过调用附件传递
- MQ:通过消息头传递
- HTTP:通过 Header 传递(
- 存储位置:
-
- 同步请求:使用
MDC
+ThreadLocal
- 异步线程:使用
TransmittableThreadLocal
- 同步请求:使用
- 日志集成:在日志模板中添加
%X{traceId}
- 异常处理:确保在 finally 块中清理上下文
- ID 生成规则:
// 示例:服务前缀 + 时间戳 + 随机数
"SVC-" + System.currentTimeMillis() + "-" + ThreadLocalRandom.current().nextInt(10000)
验证方式
- 查看日志输出是否包含 traceId
- 跨服务调用检查 Header 传递
- 异步任务验证 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>