TransmittableThreadLocal(TTL)简介-如何多线程传递TRACEID

1. 背景与问题

在多线程编程中,ThreadLocal 常用于保存线程本地的变量(如用户会话、日志跟踪ID等),确保每个线程独立访问自己的数据。但传统 ThreadLocal 存在一个致命缺陷
当任务提交到线程池时,ThreadLocal 的值无法自动传递到线程池的工作线程中 ,导致上下文丢失。

例如:

java 复制代码
ThreadLocal<String> context = new ThreadLocal<>();
ExecutorService executor = Executors.newFixedThreadPool(2);

context.set("user123"); // 主线程设置值
executor.submit(() -> {
    System.out.println(context.get()); // 输出 null(值丢失)
});

2. TransmittableThreadLocal 的解决方案

阿里巴巴开源的 TransmittableThreadLocal(TTL) 解决了这一问题。它通过以下机制实现线程池中的上下文传递:

  1. 任务提交时 :捕获当前线程的 TTL 值(生成快照)。
  2. 任务执行时:将快照中的值恢复到线程池的工作线程中。
  3. 任务完成后:清理工作线程中的上下文,避免污染后续任务。

3. 工作原理

  1. 提交任务

    • 调用 executor.submit(task) 时,TTL 自动调用 Transmitter.capture() 捕获当前线程的所有 TTL 值。
    • 将捕获的快照附加到任务中。
  2. 执行任务

    • 工作线程执行任务前,TTL 通过 Transmitter.replay(snapshot) 将快照值注入工作线程的 TTL
    • 任务内通过 get() 获取到的是提交时的上下文值。
  3. 清理阶段

    • 任务执行完成后,调用 Transmitter.restore() 恢复工作线程的原始上下文。

4. 典型场景:跨线程记录统一的日志标签

4.1、自定义 MDCTransmittableThreadLocal 值

java 复制代码
public class MDCTransmittableThreadLocal extends TransmittableThreadLocal<Map<String, String>> {
    /**
     * 在线程池中执行任务前调用,MDCTransmittableThreadLocal中的值赋值到本线程的MDC中
     */
    @Override
    protected void beforeExecute() {
        Map<String, String> mdc = get();
        mdc.forEach(MDC::put);
    }

    /**
     *  在线程池中执行任务后调用,清楚本线程MDC值 .MDCTransmittableThreadLocal值由线程池清理
     */
    @Override
    protected void afterExecute() {
        MDC.clear();
    }

    /**
     * 当线程第一次访问 TransmittableThreadLocal 时调用。
     * @return
     */
    @Override
    protected Map<String, String> initialValue() {
        return Maps.newHashMap();
    }
}

4.2、定义filter拦截请求

java 复制代码
public class RequestStreamFilter implements Filter {
    private static MDCTransmittableThreadLocal ttlMDC = new MDCTransmittableThreadLocal();
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            //为每一个请求创建一个ID,方便查找日志时可以根据ID查找出一个http请求所有相关日志
            String uuid=UUIDUtil.getUUID();
            MDC.put(SystemConstants.TRACE_ID_KEY,uuid );
            ttlMDC.get().put(SystemConstants.TRACE_ID_KEY, uuid);//跨线程池传递使用
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
            ttlMDC.get().clear();
            ttlMDC.remove();
        }
    }
    @Override
    public void destroy() {
    }
}

4.3、测试代码

java 复制代码
public static void test() throws Exception {
    //单一线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    //需要使用TtlExecutors对线程池包装一下
    executorService= TtlExecutors.getTtlExecutorService(executorService);
    //TransmittableThreadLocal创建
    TransmittableThreadLocal<String> name = new TransmittableThreadLocal<>();
    String s1 = UUID.randomUUID().toString();
    String s2 = UUID.randomUUID().toString();
    System.out.println("MDC1:"+s1);
    System.out.println("MDC2:"+s2);
    MDC.put("traceId", s1);
    for (int i = 0; i < 5; i++) {
        if(i%2 == 0){
            ttlMDC.get().put("traceId", s2);
        }else{
            ttlMDC.get().put("traceId", s1);
        }
        name.set("🗡光如影"+i);
        Thread.sleep(3000);
        CompletableFuture.runAsync(()-> {
            System.out.println(name.get());
            System.out.println("MDC:"+MDC.get("traceId"));
            System.out.println(Thread.currentThread().getName());
        },executorService);
    }
    name.remove();
    CompletableFuture.runAsync(()-> System.out.println(name.get()),executorService);
    executorService.shutdown();
}

参考

Slf4j MDC使用transmittable-thread-local解决多线程日志跟踪(转载)

相关推荐
cainiao0806052 小时前
《Spring Boot 4.0新特性深度解析》
java·spring boot·后端
-曾牛2 小时前
Spring AI 与 Hugging Face 深度集成:打造高效文本生成应用
java·人工智能·后端·spring·搜索引擎·springai·deepseek
南玖yy2 小时前
C/C++ 内存管理深度解析:从内存分布到实践应用(malloc和new,free和delete的对比与使用,定位 new )
c语言·开发语言·c++·笔记·后端·游戏引擎·课程设计
计算机学姐3 小时前
基于SpringBoot的小区停车位管理系统
java·vue.js·spring boot·后端·mysql·spring·maven
BUG制造机.3 小时前
Go 语言 slice(切片) 的使用
开发语言·后端·golang
小鸡脚来咯3 小时前
请求参数:Header 参数,Body 参数,Path 参数,Query 参数分别是什么意思,什么样的,分别通过哪个注解获取其中的信息
java·spring boot·后端
天上掉下来个程小白4 小时前
添加购物车-02.代码开发
java·服务器·前端·后端·spring·微信小程序·苍穹外卖
幽络源小助理5 小时前
懒人美食帮SpringBoot订餐系统开发实现
java·spring boot·后端·美食
源码云商8 小时前
基于Spring Boot + Vue的母婴商城系统( 前后端分离)
java·spring boot·后端
还听珊瑚海吗11 小时前
基于SpringBoot的抽奖系统测试报告
java·spring boot·后端