当前位置: 首页 > news >正文

Spring Boot实现日志链路追踪

文章目录

    • 分析及实现过程:
        • 什么是MDC
        • 常用API
        • 代码实现
        • 再谈@Async

目的:有时候一个业务调用链的场景很长,其中调了各种各样的方法,现在需要把同一次业务调用链上的日志串起来,也就是给同一个 trace ID 即可。


分析及实现过程:

什么是MDC

1、MDC(映射诊断上下文)是一个把键值对(Map<String,String>)绑定到当前线程上的机制。主要用于把请求级别或线程级别的上下文信息(如 traceId、userId等)自动带到日志输出中,便于日志追踪与排查。它是 SLF4J 提供的 API

2、MDC 在大多数实现里基于 ThreadLocal<Map<String,String>>。也就是说,MDC 的数据绑定到 线程 上,只有当前线程能看到自己的 MDC

3、大多数 Spring Boot 项目默认已经包含:spring-boot-starter-logging(内部包含 Logback 和 slf4j),因此通常无需额外引依赖。

使用:MDC.put("TRACE_ID", "xxx"),则在日志格式里用 %X{TRACE_ID}(Logback)或 ${ctx:TRACE_ID}(Log4j2)取值。


常用API

常见方法(都来自 org.slf4j.MDC):

  • MDC.put(String key, String value):设置键值对到当前线程 MDC。
  • MDC.get(String key):获取当前线程中 key 对应的值。
  • MDC.remove(String key):移除某个 key。
  • MDC.clear():清空当前线程全部 MDC。
  • MDC.getCopyOfContextMap():获取当前线程 MDC 的 拷贝(返回 Map<String,String>null)。用于把父线程上下文传给子线程。
  • MDC.setContextMap(Map<String,String>):把给定 map 设为当前线程的 MDC(覆盖)。

注意:尽量使用 getCopyOfContextMap() 获取拷贝,避免直接传递可变引用导致并发修改问题


代码实现

可以参考上传到 Gitee 中的代码:完整代码
下面贴出主要代码,并附带分析:

1、再来看看主要的实现代码

1.1 先自定义线程池,并配置线程池

/*** 自定义ThreadPoolTaskExecutor线程池*/
public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {// 重写父类的方法,进行任务包装:为了将父线程的MDC传进去线程池@Overridepublic void execute(Runnable task) {super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));}@Overridepublic Future<?> submit(Runnable task) {return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));}@Overridepublic <T> Future<T> submit(Callable<T> task) {return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));}
}
/*** 线程池配置*/
@Configuration
public class MyThreadPoolTaskExecutorConfig {@Bean("MyExecutor")public Executor asyncExecutor() {MyThreadPoolTaskExecutor myExecutor = new MyThreadPoolTaskExecutor();myExecutor.setCorePoolSize(5);myExecutor.setMaxPoolSize(5);myExecutor.setQueueCapacity(500);myExecutor.setKeepAliveSeconds(60);myExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());myExecutor.setThreadNamePrefix("ZFP");myExecutor.initialize();return myExecutor;}
}

1.2 生成并使用 traceId

public final class ThreadMdcUtil {private static final String TRACE_ID = "TRACE_ID";// 生成唯一标识public static String generateTraceId() {return UUID.randomUUID().toString();}// 假如当前线程没有traceId,就生成一个并设置public static void setTraceIdIfAbsent() {if (MDC.get(TRACE_ID) == null) {MDC.put(TRACE_ID, generateTraceId());}}/*** Callable(异步任务有返回值):父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程*/public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {return () -> {// 如果父线程传进来的(他的MDC)没有值if (context == null) {// 清理子线程的MDC,为下一步做准备MDC.clear();} else {// 有值:父线程的MDC复制给子线程MDC.setContextMap(context);}// context!=null,并且context里面有traceId,// 上面else不是做了,这里为什么还做,为了确保有TRACE_ID,兜底setTraceIdIfAbsent();try {return callable.call();} finally {MDC.clear();}};}/*** Runnable(异步任务没有返回值):父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程*/public static <T> Runnable wrap(final Runnable runnable, final Map<String, String> context) {return () -> {if (context == null) {MDC.clear();} else {MDC.setContextMap(context);}setTraceIdIfAbsent();try {runnable.run();} finally {MDC.clear();}};}
}

分析代码:

① 父线程 和 子线程

  • 父线程(调用方):当前正在运行的线程,比如处理HTTP请求的线程、主线程。它决定向线程池提交任务。
  • 子线程(执行方):真正在线程池中执行任务的线程。它不是新建的,而是线程池里复用的工作线程。

父线程是“派任务的人”,子线程是“干活的人”。父线程要告诉子线程:“你干活时要带上我的上下文(trace id、userId 等)”

② 为什么要重写线程池中的提交和执行方法?

如果不重新自定义线程池,子线程看不到父线程的MDC(也就是父线程的MDC怎么传给子线程),比如下图:

在这里插入图片描述

图中:执行 /insert 接口,流程:主线程在拦截器中设置了 MDC ----> 遇到 @Async 注解的方法,由于没有指定线程池,所以使用默认的线程池执行 ----> 子线程肯定拿不到父线程的 MDC,所以主线程有MDC,子线程为空。

所以要重写线程池中的方法,以便将父线程的 MDC 传给子线程。那怎么传?看 ③ 、④

③ 为什么要写这个 wrap 方法?

  • MDC是基于ThreadLocal的 ----> 所以父线程和子线程各自有独立的 MDC 存储空间 ----> 所以子线程默认 拿不到父线程的 MDC 内容
  • 而且如果线程池复用线程,还可能“串号”(也就是:子线程可能残留上一次任务的 MDC)

④ 那怎么理解 wrap 方法?

  • 父线程调用 wrap 方法(创建包装);子线程执行包装内部的代码;目的是 把父线程的 MDC 上下文(比如 traceId)传递给子线程使用

  • 代码解释:

    1. 如果父线程没传任何上下文(context == null),那清空子线程的 MDC(为下一步做准备)----> 否则,把父线程的上下文设置到子线程;

      不管哪种情况,最后都执行一次 setTraceIdIfAbsent()

    2. 当父线程 context 不为 nullMDC.setContextMap(context); 那为什么还需要setTraceIdIfAbsent(); 已经重复了 ,为什么?

      • 如果 context 已经包含了 "TRACE_ID",那 MDC.setContextMap(context) 后,MDC 里肯定已经有这个 key。 这时再调用 setTraceIdIfAbsent(),它检查到 MDC.get("TRACE_ID") != null,就不会再生成新的 traceId。也就是说它不会“重复设置”,但会进行一次判断。
      • 兜底检查:
        • 假如父线程 context 内容为:{ "USER_ID": "u001" }(父线程没 TRACE_ID),子线程 setTraceIdIfAbsent() 会生成一个新的 traceId(兜底);
        • 假如无任何 context(父线程没传 MDC),MDC.clear() 后执行 setTraceIdIfAbsent(),生成一个新的 traceId

      总结:setTraceIdIfAbsent() 的存在是为了“防御”那些没有带 traceId 的任务场景,保证所有线程都有唯一标识。(虽然在拦截器中设置了,但防止意外)

5、运行验证:

① 正常没有异步任务的接口:/pay

在这里插入图片描述

② 有异步任务的接口:/insert

在这里插入图片描述

本来是正常的结果,但是有个问题:就是正常实现了,而且我没指定线程池,它自己就使用了我的线程池,按道理来说,应该使用默认的线程池(SimpleAsyncTaskExecutor,然后实现不了)


再谈@Async

1、当 Spring 处理 @Async 时:如果容器里存在一个(或可识别的)Executor/TaskExecutor bean,Spring 会把它当作默认执行器来使用。所以你“没显式在 @Async 指定线程池”也会用到你定义的线程池 —— 因为 Spring 自动选了容器里那个合适的 Executor 作为默认。

2、@Async 的执行器来源优先级:

  • 如果 @EnableAsync 的配置类实现了 AsyncConfigurer,则 getAsyncExecutor() 返回的 Executor 会被当作默认 executor。否则,若容器中有唯一的 Executor / TaskExecutor 类型的 bean,Spring 会把它当默认 executor。
  • 若容器中有多个 Executor,但有一个名为 taskExecutor(或有 @Primary)的 bean,则会优先使用它。
  • 最后兜底:如果没有找到可用的 executor,Spring 会创建一个 SimpleAsyncTaskExecutor 作为默认(但这通常不是你遇到的情况)。

3、定位是哪一个 Executor

@Component
public class ExecutorInspector {@Autowiredprivate ApplicationContext ctx;@PostConstructpublic void inspect() {Map<String, Executor> execs = ctx.getBeansOfType(Executor.class);System.out.println("===== Executors in context =====");execs.forEach((name, exe) -> {System.out.println("beanName=" + name + ", class=" + exe.getClass().getName());});System.out.println("================================");}
}

4、怎么灵活选择:

@Configuration
public class MyThreadPoolTaskExecutorConfig implements AsyncConfigurer { // 实现这个接口@Bean("MyExecutor")public Executor asyncExecutor() {MyThreadPoolTaskExecutor myExecutor = new MyThreadPoolTaskExecutor();myExecutor.setCorePoolSize(5);myExecutor.setMaxPoolSize(5);myExecutor.setQueueCapacity(500);myExecutor.setKeepAliveSeconds(60);myExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());myExecutor.setThreadNamePrefix("ZFP");myExecutor.initialize();return myExecutor;}// 默认的执行器@Overridepublic Executor getAsyncExecutor() {return new SimpleAsyncTaskExecutor();}
}

结束,记得点赞收藏哦!!!

http://www.dtcms.com/a/506744.html

相关文章:

  • 孝义网站建设南京网站销售
  • 网站后台都有哪些青海网站建设价格
  • 0.3、AI Agent 知识库、召回、Recall、Embedding等 相关的概念
  • 外卡收单那点事儿之Visa篇(3)
  • 虚拟机安装ikuai系统相关配置
  • VTK开发笔记(七):示例Cone4,一数据源多演员模式,在Qt窗口中详解复现对应的Demo
  • Java开发—JDK的安装和版本管理(macOS)
  • 初次使用remix
  • 文献阅读翻译工具的选择
  • YOLOv1原理介绍
  • c 网站建设教程win7 搭建iss网站
  • 镇江软件公司南宁网站建设优化案例
  • 火电厂VR安全培训系统有哪些:广州华锐互动构建 “安全元宇宙” 生态
  • 【计算机科学与应用】基于多光谱成像与边缘计算的物流安全风险预警模式及系统实现
  • 分布式架构未来趋势:从云原生到智能边缘的演进之路
  • 云原生系列Bug修复:Docker镜像无法启动的终极解决方案与排查思路
  • 元宇宙赋能智慧城市:重构城市治理与生活新生态
  • 算法笔试题具体在考什么领域的知识?计算机科学领域的基础:数据结构,计算机组成原理,操作系统,计算机网络
  • HarmonyOS安全与隐私:权限申请与敏感数据保护实战
  • 做促销的网站珠海网站建设厚瑜
  • 基于STM32F103ZET6实现6路舵机控制
  • 【案例实战】鸿蒙分布式智能办公应用的架构设计与性能优化
  • 嘉兴网站排名优化价格环保网站 中企动力建设
  • 网站注册页面html网站建设邀标书
  • spring AOP失效的原因
  • 阿里云国际代理:阿里云备份如何保障数据安全?
  • Elasticsearch面试精讲 Day 25:Elasticsearch SQL与数据分析
  • Spring Boot 3零基础教程,WEB 开发 Thymeleaf 总结 热部署 常用配置 笔记44
  • 从概念到代码:4A架构(业务架构、数据架构、应用架构、技术架构)全景落地指南
  • 深入解析 ZeroMQ 请求-应答模式:服务端实现与全链路测试指南 || 测试ZMQ重连重试机制