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解决多线程日志跟踪(转载)

相关推荐
半路程序员1 天前
Go内存泄漏排查pprof和trace使用
开发语言·后端·golang
WongLeer1 天前
Go + GORM 多级分类实现方案对比:内存建树、循环查询与 Preload
开发语言·后端·mysql·golang·gorm
Victor3561 天前
Hibernate(34)Hibernate的别名(Alias)是什么?
后端
superman超哥1 天前
Rust HashMap的哈希算法与冲突解决:高性能关联容器的内部机制
开发语言·后端·rust·哈希算法·编程语言·冲突解决·rust hashmap
Victor3561 天前
Hibernate(33) Hibernate的投影(Projections)是什么?
后端
a程序小傲1 天前
【Node】单线程的Node.js为什么可以实现多线程?
java·数据库·后端·面试·node.js
奋进的芋圆1 天前
DataSyncManager 详解与 Spring Boot 迁移指南
java·spring boot·后端
计算机程序设计小李同学1 天前
个人数据管理系统
java·vue.js·spring boot·后端·web安全
Echo娴1 天前
Spring的开发步骤
java·后端·spring
追逐时光者1 天前
TIOBE 公布 C# 是 2025 年度编程语言
后端·.net