1363 字
约 4 分钟
28
TransmittableThreadLocal:解决异步线程中的用户上下文丢失问题

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
   ↓
解决异步线程中的上下文传递
TransmittableThreadLocal:解决异步线程中的用户上下文丢失问题
https://clxhxhhr.top/posts/2941/
作者
clxstart
发布于
2026-09-20
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。