实用指南:TTL+日志的MDC实现简易链路追踪

通过MDC实现简单链路追踪

需要用到的依赖

org.springframework.boot
spring-boot-starter-aop
cn.hutool
hutool-all
5.8.16
org.springframework.boot
spring-boot-starter-logging
org.projectlombok
lombok
1.18.30
provided
org.slf4j
slf4j-api
2.0.9
ch.qos.logback
logback-classic
1.4.11

Spring配置类配置日志输出格式

配置输出格式,让我们自己加的TraceId可以成功输出

logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %X{traceId} %logger{36} - %msg%n"
server:
servlet:
encoding:
charset: UTF-8
enabled: true
force: true

AOP配置类

生成我们的TraceId放到我们的slf4j的MDC里面

注意为什么我们的只在Controller下往我们的MDC里面加东西呢

因为我们的MDC是共享上下文的,它会往下传递,我们不需要再匹配Service方法再生成一个ID

package com.example.threadpool.Config;
import cn.hutool.core.util.IdUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ControllerLogAspect {
public static final String TRACE_ID = "traceId";
private static final Logger logger = LoggerFactory.getLogger(ControllerLogAspect.class);
// 定义切入点,匹配带有 @Controller、@RestController 或 @Service 注解的类中的所有方法
@Pointcut("@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Service)")
public void controllerAndServiceMethods() {
}
@Around("controllerAndServiceMethods()")
public Object around(ProceedingJoinPoint point) throws Throwable {
try {
String traceId = IdUtil.objectId();
String fullTraceId = "追踪ID:" + traceId;
MDC.put(TRACE_ID, fullTraceId);
logger.info("Generated traceId: {}", fullTraceId); // 调试日志
return point.proceed();
} finally {
MDC.remove(TRACE_ID);
}
}
}

Controller测试类

package com.example.threadpool.Config;
import cn.hutool.core.util.IdUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ControllerLogAspect {
public static final String TRACE_ID = "traceId";
private static final Logger logger = LoggerFactory.getLogger(ControllerLogAspect.class);
// 定义切入点,只匹配带有 @RestController 注解的类中的所有方法
@Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
public void controllerMethods() {
}
@Around("controllerMethods()")
public Object aroundController(ProceedingJoinPoint point) throws Throwable {
try {
String traceId = IdUtil.objectId();
String fullTraceId = "追踪ID:" + traceId;
MDC.put(TRACE_ID, fullTraceId);
logger.info("Generated traceId: {}", fullTraceId); // 调试日志
return point.proceed();
} finally {
MDC.remove(TRACE_ID);
}
}
}

Service测试类

package com.example.threadpool.Config;
public interface TestService {
public String test();
}
package com.example.threadpool.Config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
@Controller
@Slf4j
public class TestServiceImpl implements TestService {
@Override
public String test() {
log.info("这是Service的日志");
return null;
}
}

输出结果


通过拦截器和请求头实现不同服务之间TraceID的传递

package com.achobeta.intercepter;
import cn.hutool.core.util.IdUtil;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LogInterceptor implements HandlerInterceptor {
public static final String TRACE_ID = "traceId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果有上层调用就用上层的ID
String traceId = request.getHeader(TRACE_ID);
if (traceId == null) {
traceId = IdUtil.objectId();
}
MDC.put(TRACE_ID, traceId);
response.addHeader(TRACE_ID, traceId);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
MDC.remove(TRACE_ID);
}
}

异步情况下MDC的上下文丢失问题

MDC的底层是通过ThreadLocal来绑定到当前线程的

所以在做异步的时候要是不进行特殊处理就会造成MDC的上下文丢失

可以引入TTL来处理MDC的上下文丢失问题


TTL改进MDC实现链路追踪

引入TTL依赖

com.alibaba
transmittable-thread-local
2.14.2

重写TTL构造线程池方法-线程池适配器

package com.kira.scaffoldmvc.ShutDownHook;
import com.alibaba.ttl.TtlCallable;
import com.alibaba.ttl.TtlRunnable;
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.*;
public class TtlThreadPoolExecutor extends ThreadPoolExecutor {
public TtlThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public TtlThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public TtlThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public TtlThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
public void execute(Runnable command) {
// 获取当前线程的MDC上下文
Map contextMap = MDC.getCopyOfContextMap();
// 使用TtlRunnable包装,确保上下文传递
super.execute(TtlRunnable.get(() -> {
// 恢复MDC上下文
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
try {
command.run();
} finally {
// 不要在这里清除MDC,可能会影响后续任务
// 由任务本身决定何时清理
}
}));
}
@Override
public Future submit(Runnable task) {
return super.submit(wrapTask(task));
}
@Override
public  Future submit(Callable task) {
return super.submit(wrapTask(task));
}
private Runnable wrapTask(Runnable task) {
Map contextMap = MDC.getCopyOfContextMap();
return TtlRunnable.get(() -> {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
try {
task.run();
} finally {
MDC.clear();
}
});
}
private  Callable wrapTask(Callable task) {
Map contextMap = MDC.getCopyOfContextMap();
return TtlCallable.get(() -> {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
try {
return task.call();
} finally {
MDC.clear();
}
});
}
}

@Bean注册线程池

package com.kira.scaffoldmvc.ShutDownHook;
import com.alibaba.ttl.threadpool.TtlExecutors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Configuration
public class ThreadPoolConfig {
public static final int CORE_POOL_SIZE = 5;
public static final int MAX_POOL_SIZE = 10;
public static final int QUEUE_CAPACITY = 100;
public static final Long KEEP_ALIVE_TIME = 1L;
@Bean
public ThreadPoolExecutor kiraExecutor1() {
return new TtlThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.AbortPolicy()
);
}
@Bean
public ThreadPoolExecutor kiraExecutor2() {
return new TtlThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.AbortPolicy()
);
}
@Bean
public ThreadPoolExecutor kiraExecutor3() {
return new TtlThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.AbortPolicy()
);
}
}

AOP类,调用Controller时放入TraceId

package com.kira.scaffoldmvc.Trace;
import cn.hutool.core.util.IdUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ControllerLogAspect {
public static final String TRACE_ID = "traceId";
private static final Logger logger = LoggerFactory.getLogger(ControllerLogAspect.class);
// 定义切入点,匹配带有 @Controller、@RestController 或 @Service 注解的类中的所有方法
@Pointcut("@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Service)")
public void controllerAndServiceMethods() {
}
@Around("controllerAndServiceMethods()")
public Object around(ProceedingJoinPoint point) throws Throwable {
try {
String traceId = IdUtil.objectId();
String fullTraceId = "追踪ID:" + traceId;
MDC.put(TRACE_ID, fullTraceId);
logger.info("Generated traceId: {}", fullTraceId); // 调试日志
return point.proceed();
} finally {
MDC.remove(TRACE_ID);
}
}
}

Controller接口测试

package com.kira.scaffoldmvc.Trace;
import com.kira.scaffoldmvc.POJO.Result;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.*;
@RestController
@RequestMapping("/trace")
@Slf4j
public class TestTraceController {
@Resource
ThreadPoolExecutor kiraExecutor1;
@GetMapping("/test")
Result test(){
log.info("测试MDC1");
kiraExecutor1.execute(()->{
log.info("测试MDC2");
});
return Result.success(true );
}
}

测试结果

traceId没有丢失


网关生成TraceId-拦截器放入TraceId

拦截器

拦截器放入TraceId或者生成TraceId

package com.kira.scaffoldmvc.Config;
import com.kira.scaffoldmvc.CommonPool.RtaProxyContextInterceptor;
import com.kira.scaffoldmvc.Interceptor.LoginInterceptor;
import com.kira.scaffoldmvc.Trace.TraceInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
//配置我们的拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
//这个是在配置类里面来注册我们的拦截器
@Autowired
private TraceInterceptor traceInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(traceInterceptor).order(1)
.excludePathPatterns("/xxxx/xxxx","/xxxx/xxxx")//配置放行路径
.addPathPatterns("/**");//拦截路径,/**拦截所有
}
}


重写TTL构造线程池方法-线程池适配器

package com.kira.scaffoldmvc.ShutDownHook;
import com.alibaba.ttl.TtlCallable;
import com.alibaba.ttl.TtlRunnable;
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.*;
public class TtlThreadPoolExecutor extends ThreadPoolExecutor {
public TtlThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public TtlThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public TtlThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public TtlThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
public void execute(Runnable command) {
// 获取当前线程的MDC上下文
Map contextMap = MDC.getCopyOfContextMap();
// 使用TtlRunnable包装,确保上下文传递
super.execute(TtlRunnable.get(() -> {
// 恢复MDC上下文
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
try {
command.run();
} finally {
// 不要在这里清除MDC,可能会影响后续任务
// 由任务本身决定何时清理
}
}));
}
@Override
public Future submit(Runnable task) {
return super.submit(wrapTask(task));
}
@Override
public  Future submit(Callable task) {
return super.submit(wrapTask(task));
}
private Runnable wrapTask(Runnable task) {
Map contextMap = MDC.getCopyOfContextMap();
return TtlRunnable.get(() -> {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
try {
task.run();
} finally {
MDC.clear();
}
});
}
private  Callable wrapTask(Callable task) {
Map contextMap = MDC.getCopyOfContextMap();
return TtlCallable.get(() -> {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
try {
return task.call();
} finally {
MDC.clear();
}
});
}
}

@Bean注册线程池

package com.kira.scaffoldmvc.ShutDownHook;
import com.alibaba.ttl.threadpool.TtlExecutors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Configuration
public class ThreadPoolConfig {
public static final int CORE_POOL_SIZE = 5;
public static final int MAX_POOL_SIZE = 10;
public static final int QUEUE_CAPACITY = 100;
public static final Long KEEP_ALIVE_TIME = 1L;
@Bean
public ThreadPoolExecutor kiraExecutor1() {
return new TtlThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.AbortPolicy()
);
}
@Bean
public ThreadPoolExecutor kiraExecutor2() {
return new TtlThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.AbortPolicy()
);
}
@Bean
public ThreadPoolExecutor kiraExecutor3() {
return new TtlThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.AbortPolicy()
);
}
}

Controller接口测试

package com.kira.scaffoldmvc.Trace;
import com.kira.scaffoldmvc.POJO.Result;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.*;
@RestController
@RequestMapping("/trace")
@Slf4j
public class TestTraceController {
@Resource
ThreadPoolExecutor kiraExecutor1;
@GetMapping("/test")
Result test(){
log.info("测试MDC1");
kiraExecutor1.execute(()->{
log.info("测试MDC2");
});
return Result.success(true );
}
}

测试结果

posted @ 2025-08-17 20:42  yfceshi  阅读(24)  评论(0)    收藏  举报