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

相关推荐
Answer_ism6 小时前
【SpringMVC】SpringMVC拦截器,统一异常处理,文件上传与下载
java·开发语言·后端·spring·tomcat
盖世英雄酱581368 小时前
JDK24 它来了,抗量子加密
java·后端
Asthenia04129 小时前
无感刷新的秘密:Access Token 和 Refresh Token 的那些事儿
前端·后端
Asthenia04129 小时前
面试复盘:聊聊epoll的原理、以及其相较select和poll的优势
后端
luckyext9 小时前
SQLServer列转行操作及union all用法
运维·数据库·后端·sql·sqlserver·运维开发·mssql
Asthenia041210 小时前
ES:倒排索引的原理与写入分析
后端
圈圈编码10 小时前
Spring常用注解汇总
java·后端·spring
stark张宇11 小时前
PHP多版本共存终极填坑指南:一台服务器部署多实例的最佳实践
后端·php
Lian_Aseubel12 小时前
Springboot整合Netty简单实现1对1聊天(vx小程序服务端)
java·spring boot·后端
m0_7482548812 小时前
SpringBoot整合MQTT最详细版(亲测有效)
java·spring boot·后端