系统之家提供 Windows 系统、Ghost 系统、驱动与常用软件的安全下载及安装教程。 后台管理
📢 欢迎访问系统之家!所有资源均经过安全检测。

日志之MDC和异步多线程间传递线程ID

发布时间:2026-10-06 | 浏览:1
📥 下载地址(文章开头)
装机神器,可安装一切系统,纯净版,英文版,繁体版 ,精简版,原版等等等
1 MDC 1.1 简介 1.2 MDC坐标和使用 1.3 MDC实现原理 1.4 主要方法 2 多线程间使用 2.1 MDC工具类 2.2 拦截器和过滤器 2.2.1 拦截器定义和配置 2.2.2 过滤器配置 2.3 Java线程池中使用 2.3.1 配置线程池 2.3.2 使用ExecutorCompletionService方式 2.3.3 使用CompletableFuture方式 2.4 Spring线程池中使用 2.4.1 继承ThreadPoolTaskExecutor 2.4.2 配置线程池 2.5 异步线程 AsyncConfigurer 2.6 多线程间传递 TransmittableThreadLocal 2.6.1 引言 2.6.2 解决方案 2.7 HTTP调用丢失traceId 2.7.1 HttpClient 2.7.2 OKHttp 2.7.3 RestTemplate 2.8 定时任务 2.2 拦截器和过滤器 2.2.1 拦截器定义和配置 2.2.2 过滤器配置 2.3 Java线程池中使用 2.3.1 配置线程池 2.3.2 使用ExecutorCompletionService方式 2.3.3 使用CompletableFuture方式 2.3.2 使用ExecutorCompletionService方式 2.3.3 使用CompletableFuture方式 2.4 Spring线程池中使用 2.4.1 继承ThreadPoolTaskExecutor 2.4.2 配置线程池 2.4.1 继承ThreadPoolTaskExecutor 2.5 异步线程 AsyncConfigurer 2.6 多线程间传递 TransmittableThreadLocal 2.6.1 引言 2.6.2 解决方案 2.7 HTTP调用丢失traceId 2.7.1 HttpClient 2.7.2 OKHttp 2.7.3 RestTemplate 2.7.1 HttpClient 2.7.3 RestTemplate MDC ( Mapped Diagnostic Context ,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是 Web 应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。在一个用户的会话存续期间,可能有多个线程处理过该用户的请求。这使得比较难以区分不同用户所对应的日志。当需要追踪某个用户在系统中的相关日志记录时,就会变得很麻烦。 一种解决的办法是采用自定义的日志格式,把用户的信息采用某种方式编码在日志记录中。这种方式的问题在于要求在每个使用日志记录器的类中,都可以访问到用户相关的信息。这样才可能在记录日志时使用。这样的条件通常是比较难以满足的。 MDC 的作用是解决这个问题。 MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。 MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。 MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据 log4j.xml 配置样例,追踪日志自定义格式主要在 name="traceId" 的 layout 里面进行设置,我们使用 %X{traceId} 来定义此处会打印 MDC 里面 key 为 traceId 的 value ,如果所定义的字段在 MDC 不存在对应的key,那么将不会打印,会留一个占位符 点击了解Loback.xml文件解释 MDC 是 SLF4J/Logback 提供的 线程级日志上下文存储 。它内部通过 ThreadLocal<Map<String, String>> 保存上下文信息。 当在某个线程里执行 MDC.put("traceId", "xxx") 时, traceId 会存入当前线程的 ThreadLocal 中。 日志框架在输出日志时,会自动从 MDC 中获取 traceId 并填入日志模板。 不同线程的 MDC 是独立的,每个线程都有自己的上下文,不会互相干扰。 clear() :移除所有MDC get (String key) :获取当前线程 MDC 中指定 key 的值 getCopyOfContextMap() :将MDC从内存获取出来,再传给线程 put(String key, Object o) :往当前线程的 MDC 中存入指定的键值对 remove(String key) :删除当前线程 MDC 中指定的键值对 setContextMap() :将父线程的MDC内容传给子线程 MDC 异步线程间传递: 用 MDC 的put时,子线程在创建的时候会把父线程中的 inheritableThreadLocals 变量设置到子线程的 inheritableThreadLocals 中,而 MDC 内部是用 InheritableThreadLocal 实现的,所以会把父线程中的上下文带到子线程中 但在线程池中,由于线程会被重用,但是线程本身只会初始化一次,所以之后重用线程的时候,就不会进行初始化操作了,也就不会有父线程 inheritableThreadLocals 拷贝到子线程中的过程了,这个时候如果还想传递父线程的上下文的话,就要使用 getCopyOfContextMap 方法 定义MDC工具类,支持 Runnable 和 Callable 两种,目的就是为了把父线程的traceId设置给子线程 2.3.2 使用ExecutorCompletionService方式 使用 ExecutorCompletionService 实现多线程调用 点击了解更多关于ExecutorCompletionService信息 2.3.3 使用CompletableFuture方式 使用CompletableFuture实现多线程调用,其中收集CompletableFuture运行结果, 点击了解更多关于CompletableFuture信息 2.4 Spring线程池中使用 2.4.1 继承ThreadPoolTaskExecutor 继承 ThreadPoolTaskExecutor ,重写线程执行的方法。到这我们就做完了大部分的准备工作,还剩下最关键的就是让程序用到我们封装后的线程池。我们可以在声明线程池的时候,直接使用我们封装好的线程池(因为继承了ThreadPoolTaskExecutor) 点击了解Spring线程池配置
📥 下载地址(文章中间)
装机神器,可安装一切系统,纯净版,英文版,繁体版 ,精简版,原版等等等
到这我们所做的准备工作,改造工作也就结束了,剩下的就是使用了。只要在程序异步调用时,利用声明好的taskExecutor线程池进行调用,就可以在线程上下文正确传递Traceid了 2.5 异步线程 AsyncConfigurer 继承了 Spring 的 AsyncConfigurer ,并重写了 getAsyncExecutor 方法,这样在 Spring 中使用 @Async 注解开启异步线程,会自动传递MDC信息给子线程, 另外关于异步线程的异常捕获,先列举一下一般开启异步的方式: 使用 Spring 的 @Async 注解开启异步 通过 executor.execute 开启异步 通过 executor.submit 开启异步 通过 CompletableFuture 开启异步 下面针对异步子线程的异常捕获提供几种解决方案: 重写 AsyncConfigurer 的getAsyncUncaughtExceptionHandler方法,这种方式只能捕获方式A开启的异步 使用Future.get(),可以捕获方式C开启的异步 使用Completable.join()或者Completable.get(),可以捕获方式D开启的异步 重写getAsyncExecutor方法时,在runnable.run()代码块上使用try/catch,可以捕获方式A,B,C开启的异步 使用try/catch包裹整个runnable函数式接口,这样可以捕获A,B,C,D开启的异步 2.6 多线程间传递 TransmittableThreadLocal 假如使用 logback/log4j 官网推荐的方案,显示调用 MDC.getCopyOfContextMap() 和 MDC.setContextMap() ,在向线程池提交任务的时候需要显示的去调用。这种方式很繁琐,而且侵入性很高,可维护性也很低。 如果使用阿里的 TransmittableThreadLocal 方案,是使用 TransmittableThreadLocal 的实现去增强 ThreadPoolExecutor ,不需要在任务提交运行的时候去显示的调用MDC,但是 TransmittableThreadLocal 的官网上没有明确的结合MDC的教程。 主要有2种,一种是自己实现一个 MDCAdapter 替换 logback/log4j的MDCAdapter ,内部将其 ThreadLocal 替换为 TransmittableThreadLocal 的实现,在通过其他方式注入到日志框架中。 另外一种方式是使用 logback-mdc-ttl 来更换项目中的logback框架,内部的思路和上面类似,也是替换了 MDCAdapter 的实现。 但是这2种方式都有很大的问题,第一种需要修改日志框架的注入实现,在后续升级日志框架有很大的风险。第二种方式是引入了一个三方的日志框架,不可维护。 总结来看上述几种解决方案都不太理解,第二种方式虽然使用了 TransmittableThreadLocal 解决了包装类的问题,但是没有很好的适配MDC,修改了大量的实现代码,而且不利于后续的升级维护。 在搜索的相关的资料、源码以及 TransmittableThreadLocal 的issue里,发现了一种比较简洁的实现方式。 添加 HandlerInterceptor 拦截器,核心的实现思路是实现 TransmittableThreadLocal 的 initialValue,beforeExecute,afterExecute 接口,在多线程数据传递的时候,将数据复制一份给MDC。 使用 TransmittableThreadLocal 提供的包装池, 2.7 HTTP调用丢失traceId 在使用 HTTP 调用第三方服务接口时 traceId 将丢失,需要对 HTTP 调用工具进行改造,在发送时在 request header 中添加traceId,在下层被调用方添加拦截器获取 header 中的 traceId 添加到MDC中 HTTP 调用有多种方式,比较常见的有 HttpClient、OKHttp、RestTemplate ,所以只给出这几种HTTP调用的解决方式 2.7.1 HttpClient 实现HttpClient拦截器 实现 HttpRequestInterceptor 接口并重写 process 方法 如果调用线程中含有 traceId ,则需要将获取到的 traceId 通过 request 中的 header 向下透传下去 为HttpClient添加拦截器 通过 addInterceptorFirst 方法为 HttpClient 添加拦截器 实现 Interceptor 拦截器,重写interceptor方法,实现逻辑和HttpClient差不多,如果能够获取到当前线程的traceId则向下透传 为OkHttp添加拦截器,调用 addNetworkInterceptor 方法添加拦截器 2.7.3 RestTemplate 实现RestTemplate拦截器 实现 ClientHttpRequestInterceptor 接口,并重写 intercept 方法,其余逻辑都是一样的不重复说明 为RestTemplate添加拦截器,调用 setInterceptors 方法添加拦截器 仅处理 XXL-Job 的定时任务,利用全局 AOP 切面自动加 traceId ,避免每个定时任务都去加
📥 下载地址(文章结尾)
装机神器,可安装一切系统,纯净版,英文版,繁体版 ,精简版,原版等等等