问题描述
为了解决ThreadLocal数据在微服务线程中传递的问题,项目工程里引入了TransmittableThreadLocal
,由于使用不当,出现了ThreadLocal中的数据未成功传递到子线程中的现象。 简单说明一下问题出现的场景:
- 使用ThreadPoolTaskExecutor创建和管理线程池
- 通过spring
HandlerInterceptor
拦截请求头中的用户token,放入TransmittableThreadLocal
- controller接收到用户请求后,使用异步线程处理业务逻辑,在异步线程中需要获取
TransmittableThreadLocal
中的用户token。 - 当线上用户请求量较大,线程池无空闲线程,
HandlerInterceptor
中已经将用户token写入了ThreadLocal中,controller将异步任务提交到线程任务队列,就返回成功 - 任务队列中的任务开始执行,从
TransmittableThreadLocal
中获取token,结果未获取到
场景复现
使用简单代码复现一下问题场景:
- 先创建一个只有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
任务。
使用ThreadPoolTaskExecutor
的setTaskDecorator()
方法统一装饰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);
}
}
}