weblog/doc/7、Gateway 网关搭建与接口鉴权/7.10 使用阿里 TransmittableThreadLocal:解决异步获取上下文问题.md
2025-02-17 11:57:55 +08:00

5.9 KiB
Raw Blame History

上小节 的末尾,小哈给大家留了个小思考:ThreadLocal 应对上下文数据传递的场景,真的就一劳永逸的吗?其实不是,再来看一下 ThreadLocal 的定义:

什么是 ThreadLocal ?

ThreadLocal 是 Java 中用于创建线程局部变量的工具,每个线程都有自己的独立变量副本,不会相互干扰。

ThreadLocal 存在的问题

如果说,我们写业务代码的时候,有些逻辑是需要在异步线程中去执行,如下图所示,异步线程中,再通过 ThreadLocal 去获取上下文数据,就失效了:

小伙伴们,可以编辑 UserServiceImpl 实现类,修改代码如下,亲测一波看看:

    @Resource(name = "taskExecutor")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    
    // 省略...
    
    /**
     * 退出登录
     *
     * @return
     */
    @Override
    public Response<?> logout() {
        Long userId = LoginUserContextHolder.getUserId();

        log.info("==> 用户退出登录, userId: {}", userId);

        threadPoolTaskExecutor.submit(() -> {
            Long userId2 = LoginUserContextHolder.getUserId();
            log.info("==> 异步线程中获取 userId: {}", userId2);
        });

        // 退出登录 (指定用户 ID)
        StpUtil.logout(userId);

        return Response.success();
    }

重启认证服务,调用登出接口,看看效果,如下图所示,异步线程中获取用户 ID 显示为 null :

InheritableThreadLocal 好使不?

有的小伙伴会说,用 InheritableThreadLocal 应该就行了吧!

什么是 InheritableThreadLocal ?

InheritableThreadLocal 是 Java 提供的另一个特殊的类,它是 ThreadLocal 的子类。与 ThreadLocal 不同,InheritableThreadLocal 允许线程将其父线程中的值传递给其子线程。这对于一些需要在父子线程之间共享数据的场景非常有用。

同样的,我们来测试一下,写个 main 方法,代码如下:

    public static void main(String[] args) {
    
    	// 初始化 InheritableThreadLocal
        ThreadLocal<Long> threadLocal = new InheritableThreadLocal<>();
        
        // 假设用户 ID 为 1
        Long userId = 1L;
        
        // 设置用户 ID 到 InheritableThreadLocal 中
        threadLocal.set(userId);
        
        System.out.println("主线程打印用户 ID: " + threadLocal.get());

		// 异步线程
        new Thread(() -> {
            System.out.println("异步线程打印用户 ID: " + threadLocal.get());
        }).start();
    }

执行该方法,观察控制台输出,可以看到异步线程中成功打印了用户 ID

哎,既然如此,把 LoginUserContextHolder 中的 ThreadLocal 改成 InheritableThreadLocal , 问题不就解决了吗!其实不然,InheritableThreadLocal 同样有它的弊端,如果我们在使用线程池的情况下,它就不好使了。

阿里 TransmittableThreadLocal

TransmittableThreadLocal 是阿里巴巴开源的一个库,专门为了解决在使用线程池或异步执行框架时,InheritableThreadLocal 不能传递父子线程上下文的问题。TransmittableThreadLocal 能够将父线程中的上下文在子线程或线程池中执行时也能够保持一致。

添加依赖

编辑项目最外层 pom.xml 文件,声明 TransmittableThreadLocal 的依赖,以及版本号,代码如下:

	<properties>
		// 省略...
        <transmittable-thread-local.version>2.14.2</transmittable-thread-local.version>
    </properties>
    
        <!-- 统一依赖管理 -->
    <dependencyManagement>
        <dependencies>
			// 省略...

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>transmittable-thread-local</artifactId>
                <version>${transmittable-thread-local.version}</version>
            </dependency>


        </dependencies>
    </dependencyManagement>

接着,编辑认证服务的 pom.xml 文件,引入该依赖:

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>transmittable-thread-local</artifactId>
            </dependency>

重新刷新一下 Maven 依赖,将包下载到本地 Maven 仓库中。

修改 LoginUserContextHolder

然后,编辑 LoginUserContextHolder 上下文工具类,将 ThreadLocal 修改为 TransmittableThreadLocal , 如下图所示:

代码如下:

    // 初始化一个 ThreadLocal 变量
    private static final ThreadLocal<Map<String, Object>> LOGIN_USER_CONTEXT_THREAD_LOCAL
            = TransmittableThreadLocal.withInitial(HashMap::new);

测试一波

重启认证服务,再次请求登出接口,观察控制台日志,看看这次异步线程中能否正确获取到用户 ID, 如下图所示OK, 问题解决

小作业

最后,给大家留个小作业,由于后续其他服务也需要用到上下文工具类,完全可以将它抽取成一个 Starter 组件,到时候其他服务只需引入 Starter 组件,即可拥有此能力。

关于如何自定义 Starter , 小伙伴们可参考《4.3 小节》 ,自己动手尝试做一下,遇到问题的,也可以下载本小节的源码参考,源码已经抽取完毕,大体结构如下:

本小节源码下载

https://t.zsxq.com/iTYHA