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);
        }
    }
}
相关推荐
小码编匠1 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平3 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码4 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞4 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod4 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。5 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man6 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*6 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go