利用TransmittableThreadLocal优雅地传递线程池中父子线程的上下文

1,551 阅读5分钟

前言

之前就说过要发一篇关于阿里TTL(TransmittableThreadLocal)的文章,经过我一段时间的学习,打算总结总结分享给大家。

20221011001613_e5a34.gif

辨析TL、ITL、TTL

1.ThreadLocal:

我们都知道TL(ThreadLocal)是用来存储当前线程的上下文信息的,但是在异步的情况下父子线程中子线程无法获取父线程的该信息。

@Test
public void test3() throws Exception {
    //单一线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    //ThreadLocal存储
    ThreadLocal<String> username = new ThreadLocal<>();
    username.set("为了岩王帝君");
    Thread.sleep(3000);
    new Thread(()-> System.out.println(username.get()+" 1")).start();
    for (int i = 0; i < 2; i++) {
        CompletableFuture.runAsync(() -> System.out.println(username.get() + " 2"), executorService);
    }
    Thread.sleep(3000);
    System.out.println(username.get()+" 3");
    executorService.shutdown();
}

结果如下:

Snipaste_2024-11-28_19-26-33.png

很明显不管是新创建的子线程还是线程池复用的子线程都获取不到父线程的上下文信息。

2.InheritableThreadLocal:

随后就有了ITL(InheritableThreadLocal)的出现,ITL确实解决了父子线程间的信息传递,但是,如果我们使用的是线程池的方式,难免不会碰到线程的复用,这个时候就不一定能够实现父子线程间信息传递了,因为ITL的实现方式是在子线程初始化的时候把进行信息拷贝传递。如果使用了线程池,那些复用的线程已经被初始化了,就不会经过这个拷贝的过程。

@Test
public void test2() throws Exception {
    //单一线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    //InheritableThreadLocal存储
    InheritableThreadLocal<String> name = new InheritableThreadLocal<>();
    for (int i = 0; i < 5; i++) {
        name.set("为了岩王帝君"+i);
        Thread.sleep(3000);
        new Thread(()-> System.out.println(name.get()+" 斩尽牛杂")).start();
        CompletableFuture.runAsync(()-> System.out.println(name.get()),executorService);
    }
    name.remove();
    Thread.sleep(3000);
    System.out.println("---------------");
    CompletableFuture.runAsync(()-> System.out.println(name.get()+" 天动万象"),executorService);
    executorService.shutdown();
}

结果如下:

Snipaste_2024-11-28_19-24-11.png

很明显这些新创建的子线程都继承了父线程的上下文信息,而复用的子线程却一直是第一次的信息。

注意的是:这里父线程remove之后,第二个异步任务的子线程仍然能获取到,久而久之有内存泄漏风险。不过这里可以用try-finally包一下异步任务,在finally中手动remove一下。但是这种方式显然可能会忘记不优雅。

3.TransmittableThreadLocal:

之后就有了TTL(TransmittableThreadLocal)的出现,它是对ITL的扩展,它通过调用capture()方法捕获调用方的本地线程变量替换到线程池所对应获取到的线程的本地变量中。

@Test
public void test() throws Exception {
    //单一线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    //需要使用TtlExecutors对线程池包装一下
    executorService= TtlExecutors.getTtlExecutorService(executorService);
    //TransmittableThreadLocal创建
    TransmittableThreadLocal<String> name = new TransmittableThreadLocal<>();
    for (int i = 0; i < 5; i++) {
        name.set("🗡光如影"+i);
        Thread.sleep(3000);
        CompletableFuture.runAsync(()-> System.out.println(name.get()),executorService);
    }
    name.remove();
    CompletableFuture.runAsync(()-> System.out.println(name.get()),executorService);
    executorService.shutdown();
}

结果如下:

Snipaste_2024-11-28_19-25-28.png

很明显TTL能优雅的解决这一切。父线程remove后,子线程也不会有了。

TTL的应用场景

官网地址:github.com/alibaba/tra…

官网中也列出了几种TTL的经典应用场景:

TTL的使用

定义一个用户身份信息上下文

public class SecurityContextHolder {

    //使用TTL存储身份信息
    private static final TransmittableThreadLocal<User> THREAD_LOCAL = new TransmittableThreadLocal<>();
    
    //根据项目需要还可以存储一些其他的信息,如traceID等一些记录日志用的
    ...
    
    public static void set(User user){
        THREAD_LOCAL.set(user);
    }

    public static User get(){
        return THREAD_LOCAL.get();
    }

    public static void remove(){
        THREAD_LOCAL.remove();
    }

}

定义一个拦截器,在请求到达controller之前将信息set到TTL中,并在请求结束时清除TTL防止内存泄露

@Component
public class AuthInterceptor implements AsyncHandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    
        //获取请求头中的加密的用户信息
        String token = request.getHeader(OAuthConstant.TOKEN_NAME);
        if (StrUtil.isBlank(token))
            return true;
        //解密
        String json = Base64.decodeStr(token);
        //将json解析成User
        User user = TokenUtils.parseJsonToUser(json);
        //封装数据到TTL中
        SecurityContextHolder.set(User);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex){
        SecurityContextHolder.remove();
    }
}

后续就可以直接从TTL中取信息了,同样在后续的异步任务中可以优雅地实现线程池中父子线程的上下文传递。

对上篇文章的补充

上篇文章讲的是RocketMQ的事务消息机制,这里对其例子补充一下库存预扣减的实现方案。

这里的库存预扣减指的是:当用户下单时会先检查redis中的库存数量,库存数够的话就减少相应的库存,并增加一个冻结计数器。例如,如果用户购买了1台手机,Redis中的库存数量将减少1,同时冻结库存计数器增加1。

  1. 支付成功:当用户完成支付后,会将Redis中冻结计数器清零,并更新数据库中的库存数量,完成库存的最终扣减。
  2. 支付失败或订单取消:如果用户支付超时或取消订单,系统将释放Redis中冻结的库存(将计数器的值加回redis存的库存数),并更新数据库,恢复原始库存数量。

现在的问题就是什么时机去做这些操作呢?

我的想法是可以借助我上一篇文章的事务消息机制,本地事务mysql下单成功后,执行完对应的redis库存预扣减和计数器操作后发一个延迟消息(一般订单时间是15分钟内支付),然后消费者15分钟后收到消息后就去检查本地交易服务的订单支付状态,未支付就去恢复redis冻结库存,取消订单。

注意的是这里还有一个问题:如果用户该订单早就支付成功了,可是这个延迟消息还要很久,这显然是不合理的,所以我就想在事务消息监听器的回查方法中加一个逻辑,如果检查订单支付成功就立刻更改交易服务订单支付状态,将Redis中冻结计数器清零,并更新数据库中的库存数量,完成库存的最终扣减。即使后续延迟消息被消费者获取后,发现订单已支付,就不会继续后续逻辑了。

上述文章可能有点小瑕疵还请见谅。

d30eca9e3f0f84bf.gif