TransmittableThreadLocal数据丢失问题

问题描述

为了解决ThreadLocal数据在微服务线程中传递的问题,项目工程里引入了TransmittableThreadLocal,由于使用不当,出现了ThreadLocal中的数据未成功传递到子线程中的现象。 简单说明一下问题出现的场景:

  1. 使用ThreadPoolTaskExecutor创建和管理线程池
  2. 通过spring HandlerInterceptor拦截请求头中的用户token,放入TransmittableThreadLocal
  3. controller接收到用户请求后,使用异步线程处理业务逻辑,在异步线程中需要获取TransmittableThreadLocal中的用户token。
  4. 当线上用户请求量较大,线程池无空闲线程,HandlerInterceptor中已经将用户token写入了ThreadLocal中,controller将异步任务提交到线程任务队列,就返回成功
  5. 任务队列中的任务开始执行,从TransmittableThreadLocal中获取token,结果未获取到

场景复现

使用简单代码复现一下问题场景:

  1. 先创建一个只有3个线程的线程池
  2. 依次提交3个父任务,每个父任务中又提交一个子任务
  3. 在父任务中设置ttl,并在子任务中获取ttl
java 复制代码
public class TtlTest {

    private static TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();

    public static ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(3);
        executor.setCorePoolSize(3);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(10);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

    public static void main(String[] args) {
        ThreadPoolTaskExecutor executor = threadPoolTaskExecutor();
        executor.initialize();
        executor.execute(() -> {
            ttl.set("1");
            System.out.printf("1-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            executor.execute(() -> {
                System.out.printf("2-%s-%s\n", Thread.currentThread().getName(), ttl.get());
                ttl.remove();
                System.out.printf("2-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            });
            sleep(1000);
            ttl.remove();
            System.out.printf("1-%s-%s\n", Thread.currentThread().getName(), ttl.get());
        });
        executor.execute(() -> {
            sleep(1000);
            ttl.set("3");
            System.out.printf("3-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            executor.execute(() -> {
                System.out.printf("4-%s-%s\n", Thread.currentThread().getName(), ttl.get());
                sleep(2000);
                ttl.remove();
                System.out.printf("4-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            });
            sleep(3000);
            ttl.remove();
            System.out.printf("3-%s-%s\n", Thread.currentThread().getName(), ttl.get());
        });
        sleep(2000);
        executor.execute(() -> {
            ttl.set("5");
            System.out.printf("5-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            executor.execute(() -> {
                System.out.printf("6-%s-%s\n", Thread.currentThread().getName(), ttl.get());
                sleep(2000);
                ttl.remove();
                System.out.printf("6-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            });
            ttl.remove();
            System.out.printf("5-%s-%s\n", Thread.currentThread().getName(), ttl.get());
        });
    }

    private static void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

上述代码执行的打印结果为:

shell 复制代码
1-ThreadPoolTaskExecutor-1-1      // 父任务1,开始执行,设置ttl
2-ThreadPoolTaskExecutor-3-1      // 子任务2,开始执行,获取ttl成功
2-ThreadPoolTaskExecutor-3-null   // 子任务2,执行完毕,释放ttl
1-ThreadPoolTaskExecutor-1-null   // 父任务1,执行完毕,释放ttl
3-ThreadPoolTaskExecutor-2-3      // 父任务3,开始执行,设置ttl
4-ThreadPoolTaskExecutor-3-null   // 子任务4,开始执行,获取ttl失败!!!
5-ThreadPoolTaskExecutor-1-5      // 父任务5,开始执行,设置ttl
5-ThreadPoolTaskExecutor-1-null   // 父任务5,执行完毕,释放ttl
6-ThreadPoolTaskExecutor-1-null   // 子任务6,开始执行,获取ttl失败!!!
4-ThreadPoolTaskExecutor-3-null   // 子任务4,执行完毕,释放ttl
6-ThreadPoolTaskExecutor-1-null   // 子任务6,执行完毕,释放ttl
3-ThreadPoolTaskExecutor-2-null   // 父任务3,执行完毕,释放ttl

结果显示,子任务4和6,都没能获取到父任务中的ttl值。

解决方案

使用TtlRunnable包装Runnable

java 复制代码
TtlRunnable ttlRunnable = TtlRunnable.get(runnable, true)

TtlRunnable在构造函数中,会将当前线程的Ttl快照复制一份,传入到子线程中。

使用TtlExevutors包装ExecuteService

java 复制代码
ThreadPoolTaskExecutor executor = threadPoolTaskExecutor();
executor.initialize();
ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(executor.getThreadPoolExecutor());
// 使用包装后的ttlExecutorService来提交任务
ttlExecutorService.execute(() -> {});

在源码中,ttlExecutorService会将传入的Runnable任务,转换为TtlRunnable任务。

使用ThreadPoolTaskExecutorsetTaskDecorator()方法统一装饰Runnable

由于项目代码中多处使用了ThreadPoolTaskExecutor,为了避免较多的代码修改,项目中没有使用TtlExevutors,而是通过ThreadPoolTaskExecutor提供的setTaskDecorator()方法,统一封装任务。

java 复制代码
public static ThreadPoolTaskExecutor threadPoolTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setMaxPoolSize(3);
    executor.setCorePoolSize(3);
    executor.setQueueCapacity(10);
    executor.setKeepAliveSeconds(10);
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    // 传入一个TaskDecorator的实现
    executor.setTaskDecorator(runnable -> TtlRunnable.get(runnable, true));
    return executor;
}

结果验证

shell 复制代码
1-ThreadPoolTaskExecutor-1-1      // 父任务1,开始执行,设置ttl
2-ThreadPoolTaskExecutor-3-1      // 子任务2,开始执行,获取ttl成功
2-ThreadPoolTaskExecutor-3-null   // 子任务2,执行完毕,释放ttl
1-ThreadPoolTaskExecutor-1-null   // 父任务1,执行完毕,释放ttl
3-ThreadPoolTaskExecutor-2-3      // 父任务3,开始执行,设置ttl
4-ThreadPoolTaskExecutor-3-3      // 子任务4,开始执行,获取ttl成功!!!
5-ThreadPoolTaskExecutor-1-5      // 父任务5,开始执行,设置ttl
5-ThreadPoolTaskExecutor-1-null   // 父任务5,执行完毕,释放ttl
6-ThreadPoolTaskExecutor-1-5      // 子任务6,开始执行,获取ttl成功!!!
4-ThreadPoolTaskExecutor-3-null   // 子任务4,执行完毕,释放ttl
6-ThreadPoolTaskExecutor-1-null   // 子任务6,执行完毕,释放ttl
3-ThreadPoolTaskExecutor-2-null   // 父任务3,执行完毕,释放ttl

结果显示,子任务4和6,都能获取到父任务中的ttl值。

以下为最终代码

java 复制代码
public class TtlTest {

    private static TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();

    public static ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(3);
        executor.setCorePoolSize(3);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(10);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setTaskDecorator(runnable -> TtlRunnable.get(runnable, true));
        return executor;
    }

    public static void main(String[] args) {
        ThreadPoolTaskExecutor executor = threadPoolTaskExecutor();
        executor.initialize();
        executor.execute(() -> {
            ttl.set("1");
            System.out.printf("1-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            executor.execute(() -> {
                System.out.printf("2-%s-%s\n", Thread.currentThread().getName(), ttl.get());
                ttl.remove();
                System.out.printf("2-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            });
            sleep(1000);
            ttl.remove();
            System.out.printf("1-%s-%s\n", Thread.currentThread().getName(), ttl.get());
        });
        executor.execute(() -> {
            sleep(1000);
            ttl.set("3");
            System.out.printf("3-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            executor.execute(() -> {
                System.out.printf("4-%s-%s\n", Thread.currentThread().getName(), ttl.get());
                sleep(2000);
                ttl.remove();
                System.out.printf("4-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            });
            sleep(3000);
            ttl.remove();
            System.out.printf("3-%s-%s\n", Thread.currentThread().getName(), ttl.get());
        });
        sleep(2000);
        executor.execute(() -> {
            ttl.set("5");
            System.out.printf("5-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            executor.execute(() -> {
                System.out.printf("6-%s-%s\n", Thread.currentThread().getName(), ttl.get());
                sleep(2000);
                ttl.remove();
                System.out.printf("6-%s-%s\n", Thread.currentThread().getName(), ttl.get());
            });
            ttl.remove();
            System.out.printf("5-%s-%s\n", Thread.currentThread().getName(), ttl.get());
        });
    }

    private static void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
相关推荐
程序员岳焱2 小时前
Java 与 MySQL 性能优化:Java 实现百万数据分批次插入的最佳实践
后端·mysql·性能优化
麦兜*3 小时前
Spring Boot启动优化7板斧(延迟初始化、组件扫描精准打击、JVM参数调优):砍掉70%启动时间的魔鬼实践
java·jvm·spring boot·后端·spring·spring cloud·系统架构
大只鹅3 小时前
解决 Spring Boot 对 Elasticsearch 字段没有小驼峰映射的问题
spring boot·后端·elasticsearch
ai小鬼头3 小时前
AIStarter如何快速部署Stable Diffusion?**新手也能轻松上手的AI绘图
前端·后端·github
IT_10244 小时前
Spring Boot项目开发实战销售管理系统——数据库设计!
java·开发语言·数据库·spring boot·后端·oracle
bobz9654 小时前
动态规划
后端
stark张宇4 小时前
VMware 虚拟机装 Linux Centos 7.9 保姆级教程(附资源包)
linux·后端
亚力山大抵5 小时前
实验六-使用PyMySQL数据存储的Flask登录系统-实验七-集成Flask-SocketIO的实时通信系统
后端·python·flask
超级小忍5 小时前
Spring Boot 中常用的工具类库及其使用示例(完整版)
spring boot·后端
CHENWENFEIc6 小时前
SpringBoot论坛系统安全测试实战报告
spring boot·后端·程序人生·spring·系统安全·安全测试