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);
        }
    }
}
相关推荐
程序定小飞8 分钟前
基于springboot的学院班级回忆录的设计与实现
java·vue.js·spring boot·后端·spring
dreams_dream2 小时前
Django序列化器
后端·python·django
懷淰メ2 小时前
python3GUI--短视频社交软件 By:Django+PyQt5(前后端分离项目)
后端·python·django·音视频·pyqt·抖音·前后端
有意义2 小时前
从零搭建:json-server+Bootstrap+OpenAI 全栈 AI 小项目
前端·后端·llm
汤姆yu3 小时前
基于springboot的民间救援队救助系统
java·spring boot·后端·救援队
IT_陈寒3 小时前
React性能优化实战:这5个Hooks技巧让我的应用快了40%
前端·人工智能·后端
韩立学长4 小时前
基于Springboot的智慧管网灌溉系统i1agupa7(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
一 乐4 小时前
高校教务|教务管理|基于springboot+vue的高校教务管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·教务管理
August_._4 小时前
【MySQL】触发器、日志、锁机制 深度解析
java·大数据·数据库·人工智能·后端·mysql·青少年编程
BingoGo4 小时前
15 个 Eloquent 高级技巧,瞬间提升你的 Laravel 应用性能
后端·php