TransmittableThreadLocal:解决异步线程中的用户上下文丢失问题
前面我们通过:
Gateway
↓
Header
↓
Filter
↓
ThreadLocal
实现了当前登录用户 ID 的传递。
这样在 Service 中可以直接获取:
userId = 10001
不用再一层一层传递参数。
但是,当业务中出现异步线程时,普通 ThreadLocal 就会出现问题。
一、ThreadLocal 有什么问题?
ThreadLocal 的特点是:
每个线程都有自己独立的数据。
例如当前请求由线程 A 处理:
线程 A
└── userId = 10001
Controller、Service 都在线程 A 中执行,所以可以正常获取 userId。
但是如果 Service 提交了一个异步任务:
线程 A
↓
提交异步任务
↓
线程 B
由于线程发生了变化:
线程 A
userId = 10001
线程 B
userId = null
所以异步线程中再次获取当前用户 ID,就可能得到 null。
二、为什么会这样?
因为 ThreadLocal 保存的数据属于:
当前线程
而不是整个请求全局共享。
所以:
主线程
↓
ThreadLocal
↓
userId = 10001
一旦切换到另一个线程:
异步线程
↓
自己的 ThreadLocal
↓
没有 userId
这就是异步任务中上下文丢失的原因。
三、InheritableThreadLocal 能解决吗?
Java 提供了:
InheritableThreadLocal
它可以在创建子线程时,把父线程的数据传递给子线程。
例如:
父线程
userId = 10001
↓
new Thread()
↓
子线程
userId = 10001
普通创建子线程时确实可以使用。
但是实际项目中,我们更多使用的是:
线程池
线程池中的线程通常早就已经创建好了,而且会不断复用。
因此,单纯使用 InheritableThreadLocal 在线程池场景中并不能很好地解决上下文传递问题。
四、TransmittableThreadLocal 是什么?
这时候可以使用阿里开源的:
TransmittableThreadLocal
简称:
TTL
它主要解决:
在线程池、异步任务中传递 ThreadLocal 上下文的问题。
原来:
主线程
userId = 10001
↓
异步线程
userId = null
使用 TTL 后,希望实现:
主线程
userId = 10001
↓
异步任务
↓
线程池线程
userId = 10001
这样异步业务也可以继续获取当前登录用户信息。
五、为什么业务中需要它?
假设用户 10001 发布了一篇笔记。
主业务可能是:
保存笔记
同时还可能异步执行:
记录操作日志
发送通知
更新统计数据
发送消息
例如:
用户 10001
↓
发布笔记
↓
主线程保存数据
↓
异步记录操作日志
异步任务中也可能需要知道:
是谁执行了这个操作?
如果普通 ThreadLocal 获取不到 userId,就会变成:
userId = null
而 TTL 的目的,就是让异步任务仍然可以拿到:
userId = 10001
六、项目中如何改造?
之前上下文使用普通 ThreadLocal:
ThreadLocal
现在将底层实现替换为:
TransmittableThreadLocal
但是业务层的使用方式基本不用改变。
还是:
设置用户 ID
↓
setUserId()
获取用户 ID
↓
getUserId()
请求结束
↓
remove()
也就是说:
业务代码不需要关心上下文到底是 ThreadLocal 还是 TTL。
只需要通过统一的 LoginUserContextHolder 操作即可。
七、完整业务流程
现在整个用户上下文传递流程可以串起来了:
客户端
↓
携带 Token
↓
Gateway
↓
解析 userId
↓
Header 透传 userId
↓
下游服务 Filter
↓
写入用户上下文
↓
Controller
↓
Service
如果是普通同步业务:
Service
↓
直接获取 userId
如果进入异步任务:
Service
↓
提交异步任务
↓
线程池
↓
继续获取 userId
整个链路最终就是:
Token
↓
Gateway
↓
Header
↓
Filter
↓
用户上下文
↓
同步线程 / 异步线程
↓
获取当前 userId
八、ThreadLocal 和 TTL 的区别
可以简单理解成:
ThreadLocal
↓
解决同一个线程中的上下文传递
TransmittableThreadLocal
↓
进一步解决异步、线程池中的上下文传递
所以两者并不是完全不同的设计。
TTL 可以理解成:
在 ThreadLocal 思路的基础上,进一步增强了跨线程上下文传递能力。
九、总结
前面使用 ThreadLocal,是为了让业务代码方便获取当前登录用户。
但是一旦业务切换到异步线程:
ThreadLocal
↓
userId 丢失
因此引入 TransmittableThreadLocal:
主线程
↓
userId = 10001
↓
异步任务
↓
线程池
↓
仍然能够获取 userId
最终解决的问题就是:
让当前登录用户的上下文不仅能在同步业务中使用,在异步线程和线程池任务中也能够继续传递。
整个演进过程可以记成:
Header
↓
解决服务之间传递
ThreadLocal
↓
解决服务内部同步线程传递
TransmittableThreadLocal
↓
解决异步线程中的上下文传递