news 2026/9/30 3:07:20

SpringBoot微服务全链路日志TraceId追踪方案与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot微服务全链路日志TraceId追踪方案与实战

做微服务或者多模块项目的同学,应该都有过这种体验:一个用户请求从前端进来,后端调用三个服务,中间还穿插了几次Redis和数据库操作。突然线上报了个错,你去翻日志,发现只有一条孤零零的异常信息,上下文里既没有请求参数,也没有调用链路上的其他日志。你只能靠猜,靠时间戳碰运气,在四五个服务里来回翻日志,折腾一两个小时才定位到问题。

这就是我今天想聊的事——给SpringBoot项目实现全链路日志TraceId追踪。这套方案解决的就是“一次请求跨多个服务,日志怎么串起来”的问题。核心思路不复杂:给每个请求生成一个全局唯一的ID,打进所有日志里,后期靠这个ID把所有日志捞出来。但实际落地过程中,线程池传递、Feign调用、日志格式改造、关键位置埋点,每一环都有坑。这篇文章把这些步骤完整走一遍,包含可直接复制的代码和配置,适合正在做微服务拆分、以及被日志排查折磨过的兄弟参考。

1. 为什么你的日志在微服务环境下越来越难用

1.1 一次真实的生产事故排查经历

先讲个我自己的经历。前两年我们一个订单系统做了微服务拆分,拆成了订单服务、库存服务、支付服务三个模块。上线后不到一周,线上出现了一次偶发性的支付超时。用户下单后前端一直转圈,但最终订单状态居然还是支付成功的。

我一个人从订单服务的日志开始查,找到下单日志,跟着用户ID翻库存扣减记录,再跟着订单号翻支付回调解。单次请求涉及的日志散落在三台服务器的文件里,每台服务器的时间还不完全一致,要靠秒级时间戳去猜上下文。整个排查过程花了将近三个小时,最后发现是支付回调里一个空指针导致回调处理提前退出,后续重试机制又因为缺少幂等判断重复扣了库存。

这件事之后我下决心把TraceId方案落地。思路也很直接:用户每次请求进入系统时生成一个全局唯一的TraceId,无论这个请求在内部调了多少个服务、异步线程怎么切换,日志里都必须带着同一个TraceId。排查时只需要拿TraceId在所有服务的日志里搜一遍,整条链路就清晰了。

1.2 全链路日志追踪的核心目标与价值

所谓全链路日志追踪,标准做法是给一次业务请求分配全局唯一的标识符,并在整个调用过程中传递这个标识。最常见的标识体系分两层:

  • TraceId(链路ID):一次业务请求的全局唯一ID,从入口往下游传递,整个过程保持不变。
  • SpanId(单元ID):链路中每一个服务调用单元都有独立ID,多个Span按顺序串起来,就能还原出调用拓扑。

实际工程中,很多团队只做了TraceId一层,也够用。但如果你后续打算接入SkyWalking这类APM系统,建议一开始就把父子Span的概念设计进去,后面改造成本更低。

这套方案解决的核心痛点有三个:

  • 快速检索:一次请求的全部日志拥有同一个TraceId,用grep "traceId=xxx"就能跨服务捞全。
  • 异常关联:业务异常从底层抛到上层,日志里都带TraceId,可以直观看到异常在哪个服务、哪个方法触发。
  • 性能分析:通过Span的起止时间,能粗略判断每个环节耗时,定位慢服务。

一句话总结:日志从“按时间猜”变成“按ID精准捞”,排查效率至少提升一个数量级。

2. 实现方案选型:从零手写不如理解原理再动手

2.1 先搞清楚TraceId必须经过哪些环节

动手写代码之前,先把TraceId的生命周期梳理清楚。一个典型的同步请求链路是这样的:

浏览器发起请求 → 网关(或入口服务)接收 → 内部调用订单服务 → 订单服务调用库存服务 → 返回结果给前端

TraceId需要经历的环节包括:

  • 生成:在链路入口(网关或第一个服务)生成全局唯一ID。
  • 传递:在服务间调用时,通过HTTP Header携带TraceId传给下游。
  • 记录:服务内部所有日志输出统一带上TraceId。
  • 清理:请求处理完毕后,删除日志上下文中的TraceId。

如果链路中混入了异步线程池、MQ消息、定时任务,还需要额外处理线程间传递,否则子线程里日志的TraceId就断了。

2.2 四种TraceId生成方案对比

生成TraceId的方案不少,我把常见几种列一下:

方案长度有序性优点缺点
Java UUID36位无序生成简单,无需额外依赖太长,日志里占空间;无序导致索引性能差
短UUID(Hutool)32位(去横杠)无序简单好用,社区普及内容偏随机,但够用
雪花算法19位左右趋势有序短小有序,适合海量日志依赖时钟,时钟回拨时有极小概率重复
W3C Trace-Context55位左右无序标准化,云厂商兼容偏长,非必要场景有点重

我的建议是:生产环境用Hutool的IdUtil.fastSimpleUUID()或自研雪花算法实现。前者简单可靠,ID长度适中,唯一性完全够用;后者适合对日志体积和排序有极强要求的团队。不要为了炫技引入复杂方案,TraceId的核心是全局唯一,不重复就行。

2.3 关键技术底座:MDC(Mapped Diagnostic Context)

选定了ID生成方式,接下来要解决“日志怎么自动带TraceId”的问题。这里必须提到SLF4J提供的MDC机制。

MDC全称Mapped Diagnostic Context,是日志框架提供的一种线程绑定的KV存储,本质是一个ThreadLocal。你可以往MDC里放值,比如:

MDC.put("traceId", "abc123");

然后在日志配置文件的pattern里用占位符引用它:

<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [%X{traceId}] - %msg%n</pattern>

%X{traceId}就是MDC中key为traceId的值。这样所有日志输出会自动带上TraceId,业务代码里不需要手动拼接。

MDC既然是ThreadLocal,就要注意两点:子线程默认拿不到父线程的MDC值;线程池复用时,MDC里的旧值要记得清理。这两点后面单独说。

2.4 为什么不推荐用Spring Cloud Sleuth或SkyWalking方案

看到这里可能有人问:现在不是有Spring Cloud Sleuth、Micrometer Tracing,还有SkyWalking吗,为什么还要自己写?

我的看法是:轻量场景手写一套完全够用,而且可控性更强。Sleuth 3.0之后更名为Micrometer Tracing,对Spring Boot 3.x和Spring Cloud版本有强依赖,升级Spring Boot时容易连带踩坑。SkyWalking的TraceId查询确实更强,但它本质是APM系统,落地需要部署Agent、存储后端和UI,对中小团队来说运维成本偏高。

手写方案把核心逻辑收敛在一个Filter和几个拦截器里,代码量不超过200行,不引入额外组件。等你真的需要APM能力时,再在现有TraceId基础上接SkyWalking也不迟,自定义链路ID是可以和SkyWalking的TraceId做映射的。

3. 手把手实现:Filter + MDC + 日志格式改造

3.1 第一步:编写TraceIdFilter核心拦截逻辑

先创建一个过滤器,作为TraceId的入口。这里有几个细节需要特别注意,我直接在代码里标注了:

import org.slf4j.MDC; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.UUID; /** * 全链路日志TraceId过滤器 */ @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class TraceIdFilter extends OncePerRequestFilter { /** 请求头中传递TraceId的key */ public static final String TRACE_ID_HEADER = "X-Trace-Id"; /** MDC中存放TraceId的key */ public static final String MDC_TRACE_ID_KEY = "traceId"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // 优先取上游传递的TraceId,保证链路串联 String traceId = request.getHeader(TRACE_ID_HEADER); if (traceId == null || traceId.trim().isEmpty()) { traceId = generateTraceId(); } MDC.put(MDC_TRACE_ID_KEY, traceId); // 响应头也带上TraceId,方便前端定位问题 response.setHeader(TRACE_ID_HEADER, traceId); filterChain.doFilter(request, response); } finally { // 必须清理MDC,避免线程池复用导致TraceId串用 MDC.remove(MDC_TRACE_ID_KEY); } } private String generateTraceId() { return UUID.randomUUID().toString().replace("-", ""); } }

几个关键决策说下:

  • 必须用OncePerRequestFilter:Spring的普通Filter在Servlet容器里可能被调用多次,但OncePerRequestFilter保证一次请求只执行一次过滤逻辑,避免重复生成TraceId。
  • @Order(Ordered.HIGHEST_PRECEDENCE):过滤器执行顺序很关键,必须放在所有业务Filter最前面,否则后续Filter的日志就已经拿不到TraceId了。
  • finally里清理MDC:这步经常被忽略。Tomcat线程池是复用的,如果不清理,下一次请求可能读到上一次的TraceId,日志会串链路。
  • 响应头带回TraceId:这个环节是加分项。前端拿到TraceId,报错时直接提交这个ID,后端能立刻定位。

3.2 第二步:改造logback日志格式

过滤器能力有了,接下来要让日志真正输出TraceId。我用的是Spring Boot最常见的logback-spring.xml配置,核心是修改pattern:

<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- 日志输出格式中加入%X{traceId} --> <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [%X{traceId}] - %msg%n"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- 异步文件Appender,生产环境建议开启 --> <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="FILE"/> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH:-logs}/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH:-logs}/app.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="ASYNC_FILE"/> </root> </configuration>

配置好之后,启动服务,访问任意接口,你会看到类似这样的输出:

2025-01-15 10:23:45.123 [http-nio-8080-exec-1] INFO com.example.OrderService - [a3f9d0c2e1b84a5f9d0c2e1b84a5f9] - 收到订单创建请求

方括号里那一串就是TraceId。以后排查问题先拿到这串ID,再在所有服务的日志目录里搜,链路就出来了。

3.3 第三步:跨服务调用传递TraceId(Feign/RestTemplate)

入口服务和网关的TraceId生成了,日志格式也改好了,但下游服务怎么拿到同一个TraceId?靠的是HTTP Header传递。这里以项目里最常用的OpenFeign为例:

import feign.RequestInterceptor; import feign.RequestTemplate; import org.slf4j.MDC; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FeignTraceIdConfig { @Bean public RequestInterceptor feignTraceIdInterceptor() { return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { // 从MDC中取出当前线程的TraceId String traceId = MDC.get("traceId"); if (traceId != null && !traceId.isEmpty()) { template.header("X-Trace-Id", traceId); } } }; } }

RestTemplate同样有拦截器机制,原理一致:

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.slf4j.MDC; import java.io.IOException; public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { String traceId = MDC.get("traceId"); if (traceId != null && !traceId.isEmpty()) { request.getHeaders().add("X-Trace-Id", traceId); } return execution.execute(request, body); } }

使用RestTemplate时注册这个拦截器即可:

RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList(new RestTemplateTraceIdInterceptor()));

下游服务不需要单独写逻辑,因为入口的TraceIdFilter会从Header读取X-Trace-Id并放入MDC。这样整条调用链的TraceId就自动串联了。

3.4 第四步:异步线程池场景保持链路贯通

MDC基于ThreadLocal,天生跨线程失效。项目中用到@Async、自定义线程池、或者CompletableFuture的,子线程里的日志就不会带TraceId了。这是全链路日志落地中最大的坑。

解决方法有两种,我推荐第二种。

方法一:手动传递

在提交任务前手动把TraceId塞到子线程,用了ThreadLocal的包装类:

// 任务提交处包装Runnable Runnable taskWithTraceId = () -> { try { MDC.put("traceId", MDC.get("traceId")); // 实际业务逻辑 } finally { MDC.remove("traceId"); } }; executor.submit(taskWithTraceId);

这种方法的问题很明显:每个提交任务的地方都要写一遍,容易漏。

方法二:用TaskDecorator统一包装线程池任务

Spring的ThreadPoolTaskExecutor提供了TaskDecorator钩子,可以统一处理所有任务提交。一次配置,全局生效:

import org.slf4j.MDC; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.TaskDecorator; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.Map; @Configuration public class ThreadPoolConfig { @Bean public ThreadPoolTaskExecutor customTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(100); executor.setThreadNamePrefix("custom-task-"); // 重点:设置TaskDecorator executor.setTaskDecorator(new MdcTaskDecorator()); executor.initialize(); return executor; } /** * 将父线程的MDC上下文拷贝到子线程 */ public static class MdcTaskDecorator implements TaskDecorator { @Override public Runnable decorate(Runnable runnable) { Map<String, String> contextMap = MDC.getCopyOfContextMap(); return () -> { try { if (contextMap != null) { MDC.setContextMap(contextMap); } runnable.run(); } finally { MDC.clear(); } }; } } }

MDC.getCopyOfContextMap()会复制当前线程的MDC全部内容,在子线程执行前放进去,执行完清理。这样即使子线程异常抛出,也不会有残留。

对于@Async注解,通过自定义TaskDecorator同样有效。配置一下AsyncConfigurer即可:

@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-task-"); executor.setTaskDecorator(new ThreadPoolConfig.MdcTaskDecorator()); executor.initialize(); return executor; } }

这套方案可以把90%以上的异步场景覆盖掉。剩下的像new Thread()手搓线程、第三方框架内部线程池,只能靠代码规范去约束了。

4. 全链路日志落地的8个典型问题与排查实录

4.1 问题速查表

手写TraceId方案踩过的坑不少,我把最高频的问题整理成一张表,方便排查时按图索骥:

现象根因解决方案
所有日志都没有TraceId没有加载logback配置文件或pattern未配置%X检查logback-spring.xml是否生效,确认pattern里有%X{traceId}
入口服务日志有TraceId,下游没有Feign/RestTemplate没加拦截器确认服务间调用是否走的是同一套HTTP客户端封装
日志TraceId串到其他请求MDC在finally中未清理在Filter的finally里调用MDC.remove
异步线程日志没有TraceIdThreadLocal子线程拿不到父线程MDC线程池配置TaskDecorator,统一复制MDC
第一个请求没有TraceId后续请求有Filter顺序错误,业务Filter先执行了给TraceIdFilter设置最高优先级
前端跨域场景自定义Header被拦截浏览器CORS预检未放行自定义Header网关/后端配置Access-Control-Allow-Headers
MQ消费者日志无法关联业务链路消息体没有携带TraceId生产者在消息Header写TraceId,消费者读取后塞入MDC
网关入口没接入Filter,TraceId一直变化请求入口在网关,但网关没生成/传递TraceId在网关最外层统一接入TraceIdFilter

4.2 网关入口接入注意点

如果项目走了网关(比如Gateway),那入口Filter必须加在网关层,而不是每个业务服务各来一个。否则用户每次请求到不同服务,生成的TraceId都不一样,链路就断了。

网关的写法基本和Filter相同,核心逻辑仍然是:读上游Header → 没有就生成 → 放入MDC → 转发下游时带上Header。需要注意网关自身的WebFlux模型是响应式的,不能用Servlet的OncePerRequestFilter,要改用WebFilter:

import org.slf4j.MDC; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.UUID; @Component public class GatewayTraceIdFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) { String traceId = exchange.getRequest().getHeaders().getFirst(TraceIdFilter.TRACE_ID_HEADER); if (traceId == null || traceId.trim().isEmpty()) { traceId = UUID.randomUUID().toString().replace("-", ""); } MDC.put(TraceIdFilter.MDC_TRACE_ID_KEY, traceId); // 将TraceId写入请求头,转发给下游服务 ServerHttpRequest mutatedRequest = exchange.getRequest() .mutate() .header(TraceIdFilter.TRACE_ID_HEADER, traceId) .build(); ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build(); return chain.filter(mutatedExchange).doFinally(signalType -> MDC.remove(TraceIdFilter.MDC_TRACE_ID_KEY)); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }

注意点:响应式场景里,MDC.remove不能写在普通finally里,因为Mono不一定被执行完,要用doFinally确保链路结束后清理。

4.3 MQ消息场景的TraceId传递

异步场景里还有一块容易断链的是消息队列。比如订单服务发消息给库存服务,如果消息里不带TraceId,库存服务侧日志就跟主链路脱节。

MQ的解决方案和HTTP类似,关键点在于消息头传递。以RocketMQ为例,生产者发送消息时把TraceId塞进Message的properties:

Message message = new Message("inventory_topic", body); message.putUserProperty("X-Trace-Id", MDC.get("traceId")); producer.send(message);

消费者接收到消息后,先从消息头里取TraceId放入MDC,再执行业务逻辑:

@Override public void onMessage(MessageExt messageExt) { String traceId = messageExt.getUserProperty("X-Trace-Id"); if (traceId == null || traceId.isEmpty()) { traceId = UUID.randomUUID().toString().replace("-", ""); } MDC.put(TraceIdFilter.MDC_TRACE_ID_KEY, traceId); try { // 消费业务逻辑 } finally { MDC.remove(TraceIdFilter.MDC_TRACE_ID_KEY); } }

Kafka的ConsumerRecord也有headers属性,同理。RabbitMQ则是用MessageProperties的headers。无非是“取→放MDC→执行→清理”四步,套路一致。

4.4 埋点检测与链路演练

TraceId方案上线前,至少要做一次完整链路的演练。我的做法是:

  1. 找一台测试环境,启动入口网关 + 订单服务 + 库存服务三个应用。
  2. 用一个带自定义Header的请求工具(比如Postman)发起一次调用,Header里写X-Trace-Id: test-10001。
  3. 依次去三个服务的日志文件里grep这个TraceId,确认每条日志都带上了。
  4. 再发起一次不带Header的请求,确认入口服务自动生成了新的TraceId,并且下游服务跟随的是同一个值。
  5. 看一条关键业务链路是否完整,比如从“收到请求”到“返回响应”之间,数据库操作、Redis缓存、远程调用这些关键节点是否都有日志。

演练过程中如果发现某段链路日志缺失,多半是以下三种原因:一是代码走了AOP切面,切面里自己打着玩没带MDC的占位符;二是某些日志是第三方SDK内部放出来的,不受logback pattern控制,这种情况只能靠SDK自身支持MDC,或者用日志增强插件截获后补充;三是异步线程内部的嵌套任务又开了线程,子线程的装饰器没生效。

4.5 几个提升排查效率的额外小技巧

方案主流程跑通之后,我建议加几个小优化,排查效率能再上一个台阶。

标记完整调用链:不光是异常日志打TraceId,请求入口、关键业务节点、外部调用、出参返回这些位置都打一条带TraceId的日志。这样拿到TraceId就能看到整个调用过程的“时间线”,比只看异常灭菌更直观。

响应头里回传TraceId:前面提到过,前端报错时只要把浏览器Network里的X-Trace-Id值发过来,后端就能直接定位。这个习惯要养起来,线上沟通成本能低很多。

日志文件按天切分并保留30天:TraceId方案再完美,日志文件被覆盖了也是白搭。logback-spring.xml里给RollingFileAppender配置maxHistory,历史日志保留至少30天。如果磁盘允许,保留60天。

关键服务接入独立的Error日志文件:可以把ERROR级别的日志单独输出到一个文件,排查线上问题时先在这个文件里搜TraceId,效率更高。配置方式给一段参考:

<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH:-logs}/error.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH:-logs}/error.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <logger name="com.example" level="INFO"/> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="ASYNC_FILE"/> <appender-ref ref="ERROR_FILE"/> </root>

5. 全链路日志追踪的扩展方向与我的实操体会

5.1 从日志追踪走向调用链分析

手写的TraceId方案解决的是“日志检索”问题,但它的数据天然可以用来做调用链分析。只要你在代码的关键节点记录当前SpanId、父SpanId、方法名、耗时,把日志汇总到ELK或Loki,就能按TraceId聚合出一次请求的完整调用拓扑。

我之前做过一个轻量版:在AOP切面里拦截所有业务方法,每次调用把方法名、参数、耗时、TraceId、SpanId拼成一条结构化日志,输出到独立文件。然后用小脚本按TraceId聚合,生成简单的调用链树。虽然比起SkyWalking差远了,但解决“这个调用耗时3秒,花在哪了”这种问题完全够用。

如果团队有预算部署SkyWalking,落地时也无需推翻现有方案。SkyWalking有原生的TraceId体系,但它的搜索框也支持自定义业务TraceId关联查询,相当于两条并行能力。

5.2 我对这套方案的真实感受

这套方案我前前后后在两个项目里落地过,代码量不大,收益却非常明显。最直观的感受是:线上排查从“碰运气猜时间窗口”变成“拿ID说话”,新人也能很快上手。以前周报里写“排查线上问题耗时X小时”,现在基本都是分钟级定位。

踩过几次坑之后,有几个习惯我一直保持到现在:

  • 所有日志一律走SLF4J接口,禁止直接调用log4j2或logback的API。这样才能保证MDC随处可用,将来切换日志实现也不受影响。
  • Filter里不用try-catch吞异常,只做try-finally。TraceIdFilter不干预业务异常传播,只在最后清理MDC,避免副作用。
  • 每个服务都要配一份TraceId上下文工具类。比如把取TraceId、生成TraceId的方法收敛到一个公共组件里,避免各服务自己各写一套,维护时才知道什么叫痛苦。

5.3 一个可以直接抄作业的优化点

最后分享一个我在项目里做的小优化:当请求是外部传入时,TraceId生成在网关或第一个服务;但如果是一次内部定时任务发起的业务,链路入口不在HTTP请求里,Filter根本不会触发。

解决方式是写一个通用的工具方法,业务入口或定时任务启动时手动调用:

public class TraceIdUtil { /** 初始化TraceId,适用于非HTTP入口 */ public static void initTraceId() { if (MDC.get("traceId") == null) { MDC.put("traceId", UUID.randomUUID().toString().replace("-", "")); } } /** 清理TraceId */ public static void clearTraceId() { MDC.remove("traceId"); } }

定时任务里这样用:

@Scheduled(cron = "0 0 2 * * ?") public void dailyReconciliationTask() { TraceIdUtil.initTraceId(); try { // 业务逻辑 log.info("每日对账任务开始执行"); // ... } finally { TraceIdUtil.clearTraceId(); } }

这样定时任务的日志也带上了TraceId,和HTTP请求走的是同一套检索逻辑。排查问题时不用再做“任务日志在另一个文件里只能盲猜”这种事了。

全链路日志TraceId这个东西,单独看每个环节都不难,难的是把所有管道都打通:HTTP入口、HTTP调用、异步线程、MQ消息、定时任务、日志格式。哪一环断了,排查的时候TraceId就搜不到完整链路,方案就算白做。如果你正准备在项目里落地这套方案,按我前文的顺序一步步来,把最后那些容易断的点都处理掉,基本就能做到一套代码全场景覆盖了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:06:58

论文结构像一团乱麻?博导推荐这几个AI写作辅助网站

写论文总是抓不住重点&#xff0c;结构混乱、逻辑不清&#xff0c;是很多学生共同的困扰。其实&#xff0c;只要用对 AI 写作辅助工具&#xff0c;再配合科学的写作流程&#xff0c;就能事半功倍。多位博导在教学中推荐过一些高效工具&#xff0c;其中千笔AI凭借中文全流程支持…

作者头像 李华
网站建设 2026/9/30 3:06:45

消息队列持久化设计:文件存储、刷盘策略与崩溃恢复实践

开头就不多废话了。先说个场景&#xff1a;你辛辛苦苦搭了一套基于内存队列的异步处理系统&#xff0c;上线跑了两周&#xff0c;一切看起来都很顺&#xff0c;结果某天深夜机房断电&#xff0c;重启之后发现积压的几千条订单消息全没了&#xff0c;数据库里对不上账。这时候你…

作者头像 李华
网站建设 2026/9/30 3:06:09

华为U1981语音网关中继配置实战:SS7与SIP全流程解析

简介&#xff1a;针对华为U1981语音网关的中继配置文档&#xff0c;面向通信网络工程师与语音网关运维人员&#xff0c;系统讲解统一网关与对端设备对接场景下的中继配置方法。文档从局向、子路由、路由、局向选择码等基础概念入手&#xff0c;逐步展开SS7与SIP中继的完整配置流…

作者头像 李华
网站建设 2026/9/30 3:05:25

2026年10款硬核降AI率工具推荐:论文AIGC检测通关率100%,无痕降AI率

随着知网、维普、万方等主流学术平台对AIGC检测标准不断收紧&#xff0c;论文的AI痕迹与查重率问题愈发受到关注。选择合适的降AI工具已成为学术写作中的关键环节。本文将实测对比10款主流工具&#xff0c;为读者提供客观、实用的参考建议。为什么需要降 AI 率工具&#xff1f;…

作者头像 李华
网站建设 2026/9/30 3:05:23

行为型设计模式实战:解决协作失序与状态爆炸

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 3:05:21

计算机网络第一章概述:从OSI到TCP/IP,五层协议与核心概念全解析

《计算机网络》这门课&#xff0c;很多人翻开第一章就喊晕&#xff1a;满页定义、满页图&#xff0c;OSI七层还没记住&#xff0c;TCP/IP四层又来了。但我必须说实话——第一章根本不是用来背的&#xff0c;它是在给你搭骨架。骨架搭对了&#xff0c;后面学路由、学TCP、学拥塞…

作者头像 李华