全场景实战:Spring Boot + MDC:实现异步线程池链路追踪
《Spring Boot 3实战案例锦集》PDF电子书已更新至130篇!
🎉🎉《Spring Boot实战案例合集》目前已更新224个案例,我们将持续不断的更新。文末有电子书目录。→ 现在就订阅合集
环境:Spring Boot 3.5.0
1. 简介
在微服务高并发业务场景中,接口往往需要并行调用多个第三方接口聚合数据,以此缩短响应耗时。SpringBoot 默认异步线程池基于 ThreadLocal 实现的 MDC 上下文,无法自动传递至子线程,导致请求链路 traceId 丢失。不同异步线程日志无法关联同一请求,线上问题排查、链路溯源变得异常困难。
本文基于自定义 ThreadPoolTaskExecutor 线程池,结合 MDC 任务装饰器实现上下文自动透传,解决跨线程日志链路追踪问题,适配多接口并行聚合的生产实战场景。
2.实战案例
在线程池能够传播 MDC 之前,我们需要先设置 traceId。如果请求中不存在唯一的ID,那么就在Filter中生成唯一的ID。
@Componentpublic class TraceIdFilter extends OncePerRequestFilter {private static final String TRACE_ID = "traceId";@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {// 从请求header中读取或者生成唯一IDString traceId = request.getHeader("X-Trace-Id");if (traceId == null || traceId.isEmpty()) {traceId = UUID.randomUUID().toString();}MDC.put(TRACE_ID, traceId);response.setHeader("X-Trace-Id", traceId);try {chain.doFilter(request, response);} finally {MDC.clear();}}}
2.2 配置线程池
我们需要自定义线程池来装饰线程执行的任务。
@Configuration@EnableAsyncpublic class TaskExecutorConfig {@Value("${price.pool.core-size:3}")private int corePoolSize;@Value("${price.pool.max-size:10}")private int maxPoolSize;@Value("${price.pool.queue-capacity:100}")private int queueCapacity;@Value("${price.pool.thread-name-prefix:price-fetch-}")private String threadNamePrefix;@Bean(name = "packTaskExecutor")Executor priceTaskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(corePoolSize);executor.setMaxPoolSize(maxPoolSize);executor.setQueueCapacity(queueCapacity);executor.setThreadNamePrefix(threadNamePrefix);// 自定义线程工厂executor.setThreadFactory(new ThreadFactory() {private final AtomicInteger counter = new AtomicInteger(1);@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r, threadNamePrefix + counter.getAndIncrement());thread.setDaemon(false);return thread;}});// 装饰线程执行的任务Runnableexecutor.setTaskDecorator(new MdcTaskDecorator());executor.setWaitForTasksToCompleteOnShutdown(true);executor.setAwaitTerminationSeconds(60) ;executor.initialize();return executor;}}
任务装饰器
public class MdcTaskDecorator implements TaskDecorator {@Overridepublic Runnable decorate(Runnable runnable) {// 1.获取当前线程的上下文数据(父线程)Map<String, String> parentMdcContext = MDC.getCopyOfContextMap();return () -> {// 2.在子线程中保留现有的 MDCMap<String, String> originalChildMdcContext = MDC.getCopyOfContextMap();try {// 3.在子线程中恢复父线程的 MDCif (parentMdcContext != null) {MDC.setContextMap(parentMdcContext);} else {MDC.clear();}// 4.执行原始任务runnable.run();} finally {// 5.恢复子线程原有的 MDC(防止泄漏)if (originalChildMdcContext != null) {MDC.setContextMap(originalChildMdcContext);} else {MDC.clear();}}};}}
总结说明:
-
提交任务前:捕获父线程的 MDC上下文(如 traceId=abc-123)
-
当任务在子线程中启动时:保存任何现有的 MDC(以防万一),然后恢复父线程的 MDC
-
任务执行时:现在,在子线程中 MDC.get("traceId") 返回 "abc-123"!
-
任务完成后:恢复子线程原有的 MDC,以防止内存泄漏
2.3 异步聚合任务
我们这里模拟分别从:淘宝、京东、拼多多 获取商品价格。
@Servicepublic class PriceAggregateService {private static final Logger log = LoggerFactory.getLogger(PriceAggregateService.class);private final Executor packTaskExecutor;public PriceAggregateService(@Qualifier("packTaskExecutor") Executor packTaskExecutor) {this.packTaskExecutor = packTaskExecutor;}public CompletableFuture<String> getTaobaoPrice(String productId) {return CompletableFuture.supplyAsync(() -> {log.info("[淘宝] 查询商品价格,productId:{}", productId);// 模拟调用接口 500msThread.sleep(500);return "淘宝:¥5999";}, this.packTaskExecutor);}public CompletableFuture<String> getJdPrice(String productId) {return CompletableFuture.supplyAsync(() -> {log.info("[京东] 查询商品价格,productId:{}", productId);Thread.sleep(500);return "京东:¥5899";}, this.packTaskExecutor);}public CompletableFuture<String> getPddPrice(String productId) {return CompletableFuture.supplyAsync(() -> {log.info("[拼多多] 查询商品价格,productId:{}", productId);Thread.sleep(500);return "拼多多:¥5299";}, this.packTaskExecutor);}}
定义Controller接口,我们将在接口中并行调用上的3个方法:
@RestController@RequestMapping("/api/price")public class PriceAggregateController {private static final Logger log = LoggerFactory.getLogger(PriceAggregateController.class);private final PriceAggregateService priceAggregateService;public PriceAggregateController(PriceAggregateService priceAggregateService) {this.priceAggregateService = priceAggregateService;}@GetMapping("/{productId}")public Map<String, Object> getPrice(@PathVariable String productId) {log.info("接收商品比价请求,productId:{}", productId);// 并行调用CompletableFuture<String> taobao = priceAggregateService.getTaobaoPrice(productId);CompletableFuture<String> jd = priceAggregateService.getJdPrice(productId);CompletableFuture<String> pdd = priceAggregateService.getPddPrice(productId);// 等待所有结果返回CompletableFuture.allOf(taobao, jd, pdd).join();Map<String, Object> result = new HashMap<>();try {result.put("taobao", taobao.get());result.put("jd", jd.get());result.put("pdd", pdd.get());}log.info("商品比价完成,productId:{}", productId);return result;}}
2.4 配置日志
要让 traceId 出现在每条日志行中,我们需要在配置 logback-spring.xml 中进行如下的配置:
<configuration scan="false" scanPeriod="15 seconds" debug="false"><contextName>common</contextName><property name="COMMON_PATTERN"value="%green(%d{23:mm:ss}) [%yellow(%thread)][%red(traceId=%X{traceId:-N/A})] %highlight(%-5level) %logger{36} - %msg%n"/><appender name="TRACEX" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>${COMMON_PATTERN}</pattern><charset>UTF-8</charset></encoder></appender><springProfile name="dev | default"><root level="INFO"><appender-ref ref="TRACEX" /></root></springProfile></configuration>
说明:%X{traceId:-N/A} 从MDC获取 (如果没有设置则显示:"N/A")。
2.5 测试

控制台日志输出:

2.5 微服务调用透传
微服务之间的调用我们通常会采用openfeign,那么要实现traceId的传递,我们需要自定义拦截器:
public class FeignTraceInterceptor implements RequestInterceptor {public static final String TRACE_ID = "traceId";public static final String X_TRACE_ID = "X-Trace-Id";@Overridepublic void apply(RequestTemplate template) {// 1. 从 MDC 获取当前线程的 traceIdString traceId = MDC.get(TRACE_ID);// 2. 如果存在,放入 Feign 调用的请求头中if (traceId != null && !traceId.isEmpty()) {template.header(X_TRACE_ID, traceId);}}}// 配置Feign@EnableFeignClients(defaultConfiguration = FeignDefaultConfiguration.class)public class FeignConfig {}@Configurationpublic class FeignDefaultConfiguration {@BeanFeignTraceInterceptor feignTraceInterceptor() {return new FeignTraceInterceptor() ;}}
2.6 全局Rest Client透传
当我们直接使用RestTemplate或者RestClient调用接口时,那么我们可以通过如下的方式定义全局的拦截器:
自定义RestClient:
@Componentpublic class CustomRestClient implements RestClientCustomizer {public static final String TRACE_ID = "traceId";public static final String X_TRACE_ID = "X-Trace-Id";@Overridepublic void customize(Builder restClientBuilder) {restClientBuilder.requestInterceptor(new ClientHttpRequestInterceptor() {@Overridepublic ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)throws IOException {String traceId = MDC.get(TRACE_ID);request.getHeaders().add(X_TRACE_ID, traceId) ;return execution.execute(request, body) ;}}) ;}}
自定义RestTemplate:
@Componentpublic class CustomRestTemplate implements RestTemplateRequestCustomizer<ClientHttpRequest> {public static final String TRACE_ID = "traceId";public static final String X_TRACE_ID = "X-Trace-Id";@Overridepublic void customize(ClientHttpRequest request) {String traceId = MDC.get(TRACE_ID);request.getHeaders().add(X_TRACE_ID, traceId) ;}}
以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏
Spring Boot+WebSocket+Redis:毫秒级实时在线系统
高级开发!手撕Controller底层调用链,深度功能扩展、性能优化
更多推荐


所有评论(0)