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);
        }
    }
}
相关推荐
csucoderlee1 小时前
Go语言中结构体字面量
开发语言·后端·golang
zimoyin7 小时前
Kotlin 使用 Springboot 反射执行方法并自动传参
spring boot·后端·kotlin
SomeB1oody9 小时前
【Rust自学】18.1. 能用到模式(匹配)的地方
开发语言·后端·rust
LiuYuHani9 小时前
Spring Boot面试题
java·spring boot·后端
萧月霖9 小时前
Scala语言的安全开发
开发语言·后端·golang
电脑玩家粉色男孩9 小时前
八、Spring Boot 日志详解
java·spring boot·后端
ChinaRainbowSea10 小时前
八. Spring Boot2 整合连接 Redis(超详细剖析)
java·数据库·spring boot·redis·后端·nosql
叫我DPT11 小时前
Go 中 defer 的机制
开发语言·后端·golang
我们的五年11 小时前
【Linux网络编程】:守护进程,前台进程,后台进程
linux·服务器·后端·ubuntu
谢大旭12 小时前
ASP.NET Core自定义 MIME 类型配置
后端·c#