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);
        }
    }
}
相关推荐
新知图书18 分钟前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放39 分钟前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang1 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net
Rverdoser2 小时前
RabbitMQ的基本概念和入门
开发语言·后端·ruby
Tech Synapse3 小时前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
.生产的驴3 小时前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
微信-since811923 小时前
[ruby on rails] 安装docker
后端·docker·ruby on rails
代码吐槽菌5 小时前
基于SSM的毕业论文管理系统【附源码】
java·开发语言·数据库·后端·ssm
豌豆花下猫5 小时前
Python 潮流周刊#78:async/await 是糟糕的设计(摘要)
后端·python·ai
YMWM_5 小时前
第一章 Go语言简介
开发语言·后端·golang