TransmittableThreadLocal相关组件实用解读,及如何达到线程池中的线程复用,及使用在哪些线程数据传递场景?

Posted 阿啄debugIT

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TransmittableThreadLocal相关组件实用解读,及如何达到线程池中的线程复用,及使用在哪些线程数据传递场景?相关的知识,希望对你有一定的参考价值。

前言

ThreadLocal只在当前线程中能访问到,其他线程隔离。InheritableThreadLocal能够传递给子线程。
对于线程池中的线程复用,将当前线程中的ThreadLocal传递给线程池中的其他线程,TransmittableThreadLocal提供了解决方案。

TransmittableThreadLocal 是Alibaba开源的、用于解决 “在使用线程池等会缓存线程的组件情况下传递ThreadLocal” 问题的 InheritableThreadLocal 扩展。

若希望 TransmittableThreadLocal 在线程池与主线程间传递,需配合 TtlRunnable TtlCallable 使用。

由于InheritableThreadLocal在线程池中上下文传递的问题,可以知道这个threadLocal的值传递,在父子线程之间若使用了线程池的技术,会导致子线程的threadLocal信息错乱。

而transmittable-thread-local简称:TTL,在使用线程池等,会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。

为框架/中间件设施开发提供的标配能力,项目代码精悍,只依赖了javassist做字节码增强,实现Agent模式下的近乎无入侵提供TTL功能的特性。

TTL整个过程的完整时序图

TransmittableThreadLocal的简单使用

package cn.search.ttl.delegate;

import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/** @Author: FLEAPX @Date: 2021/6/18 19:46 */
@Slf4j
public class TestDelegate {

  static TransmittableThreadLocal<String> TTL = new TransmittableThreadLocal<>();

  public static void main(String[] args) throws Exception {
    Runnable target = () -> log.info("target");
    Delegate level1 = new Delegate(target);
    Delegate level2 = new Delegate(level1);
    Delegate level3 = new Delegate(level2);
    level3.run();
    // ......
  }

  @RequiredArgsConstructor
  static class Delegate implements Runnable {
    public Delegate(Runnable target) {
      Integer ttlv = target.hashCode();
      System.out.println(target.getClass().getCanonicalName() + "####:" + ttlv);
      TTL.set(ttlv + "");
    }

    //		private final Runnable runnable;

    @Override
    public void run() {
      //			runnable.run();
      System.out.println("----:----" + TTL.get());
    }
  }
}

输出结果,子线程get的值,便是从父线程中获取了,TransmittableThreadLocal是采用了代理模式模板方法模式进行处理的,使用线程的情况,也需要transmittable-thread-local包中相应的类进行装饰使用。

实用源码分析

TransmittableThreadLocal 继承自 InheritableThreadLocal,这样可以在不破坏ThreadLocal 本身的情况下,使得当用户利用 new Thread() 创建线程时,仍然可以达到传递InheritableThreadLocal 的目的。

public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> { ...... }

TransmittableThreadLocal 相比较 InheritableThreadLocal 很关键的一点,改进是引入holder变量,这样就不必对外暴露Thread中的 inheritableThreadLocals,保持ThreadLocal.ThreadLocalMap的封装性

理解holder,需注意如下几点:
1、holder 是 InheritableThreadLocal 变量;
2、holder 是 static 变量;
3、value 是 WeakHashMap;
4、深刻理解 ThreadLocal 工作原理;

调用 get() 方法时,同时将 this 指针放入 holder。

调用 set() 方法时,同时处理 holder 中 this 指针。

TTL工作流程解读

自定义 TtlRunnable 实现 Runnable,TtlRunnable初始化方法中,保持当前线程中已有的TransmittableThreadLocal。

线程池中线程调用run方法,执行前,先backup holder中所有的TransmittableThreadLocalcopiedRef中不存在,holder存在的,说明是后来加进去的,remove掉holder中的,将其copied中的TransmittableThreadLocal set到当前线程中。

执行后,再恢复 backup 的数据到 holder 中(backup中不存在,holder中存在的TransmittableThreadLocal从holder中remove掉),将 backup 中的 TransmittableThreadLocal set到当前线程中。

TransmittableThreadLocal应用

分布式跟踪系统

由于TTL大量采用代理模式,如可以基于Micrometer去统计任务的执行时间,上报到Prometheus,然后用Grafana做监控和展示,再结合Agent和字节码增强(使用ASM、Javassist等),可以实现类加载时期,替换对应的Runnable、Callable或者一般接口的实现,这样就能无感知完成了增强功能,从而达到分布式跟踪系统的目的。

应用容器或上层框架跨应用代码给下层SDK传递信息

1.举个具体的业务场景,在App Engine(PAAS)上运行,由应用提供商提供的应用(SAAS模式)。
多个SAAS用户购买,并使用这个应用(即SAAS应用)。
SAAS应用往往是一个实例,为多个SAAS用户提供服务。

2.另一种模式是:SAAS用户,使用完全独立一个SAAS应用,包含独立应用实例,及其后的数据源(如DB、缓存,etc)。

需要避免的SAAS应用拿到多个SAAS用户的数据。

  • 一个解决方法是,处理过程关联好一个SAAS用户的上下文,在上下文中,应用只能处理(读/写)这个SAAS用户的数据。
  • 请求由SAAS用户发起(如从Web请求进入App Engine),App Engine可以知道,是从哪个SAAS用户,在Web请求时在上下文中设置好SAAS用户ID。
  • 应用处理数据(DB、Web、消息 etc.)是通过App Engine提供的服务SDK来完成。
  • 当应用处理数据时,SDK检查数据所属的SAAS用户,是否和上下文中的SAAS用户ID一致,如果不一致则拒绝数据的读写。

应用代码会使用线程池,并且这样的使用是正常的业务需求。
SAAS用户ID的从要App Engine传递到下层SDK,要支持这样的用法。

上面场景使用TTL的整体构架

构架涉及3个角色:容器、用户应用、SDK。

整体流程:

  • 请求进入PAAS容器,提取上下文信息并设置好上下文。
  • 进入用户应用处理业务,业务调用SDK(如DB、消息、etc)。
  • 用户应用会使用线程池,所以调用SDK的线程可能不是请求的线程。
  • 进入SDK处理。提取上下文的信息,决定是否符合拒绝处理。

整个过程中,上下文的传递 对于 用户应用代码 期望是透明的。

日志收集记录系统上下文

transmittable-thread-local在slf4j中解决MDC线程池中上下文传递。

log4j中MDC用法

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。
MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

由于MDC只适合同一台服务器的多线程的父子线程的数据传递,但是对于分布式系统的线程复用,就不适合了。这时,采用transmittable-thread-local,就恰到好处。

添加log4j2-ttl-thread-context-map依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>log4j2-ttl-thread-context-map</artifactId>
    <version>1.3.0</version>
    <scope>runtime</scope>
</dependency>

将此依赖项添加到您的项目中,
然后通过配置,

log4j2.threadContextMap=com.alibaba.ttl.log4j2.TtlThreadContextMap

即可,将MDC的adapter实现,进行替换。

logback-mdc-ttl

添加依赖:

<dependency>
    <groupId>com.ofpay</groupId>
    <artifactId>logback-mdc-ttl</artifactId>
    <version>1.0.2</version>
</dependency>

在Java的启动参数加上:
 

-Xbootclasspath/a:/path/to/transmittable-thread-local-2.x.x.jar
-javaagent:/path/to/transmittable-thread-local-2.x.x.jar

在logback配置文件中增加TtlMdcListener

<?xml version="1.0" encoding="UTF-8"?>
<configuration >
    <!-- ...(略) -->
    <contextListener class="com.ofpay.logback.TtlMdcListener"/>
    <!--例子: %X{uuid} 支持在跨线程池时传递-->
    <property scope="context" name="APP_PATTERN"
value='%d{yyyy-MM-dd HH:mm:ss.SSS}|%X{uuid}|%level|%M|%C\\:%L|%thread|%replace(%.-2000msg){"(\\r|\\n)","\\t"}|"%.-2000ex{full}"%n'/>
</configuration>


通过将contextListener的方式,将MDC的adapter实现进行替换。

Session级别的Cache

对于计算逻辑复杂业务流程,基础数据读取服务(这样的读取服务往往是个外部远程服务)可能需要多次调用,期望能缓存起来,以避免多次重复执行高成本操作。
同时,在入口发起不同的请求,处理的是不同用户的数据,所以不同发起请求之间不需要共享数据,这样也能避免请求对应的不同用户之间可能的数据污染。
因为涉及多个上下游线程,其实是Session级缓存。
通过Session级缓存有一下好处:

  1. 避免重复执行高成本操作,提升性能。
  2. 避免不同Session之间的数据污染。

参见:https://github.com/alibaba/transmittable-thread-local/blob/master/docs/requirement-scenario.md#-3-session%E7%BA%A7cache

https://github.com/alibaba/transmittable-thread-local/blob/master/docs/requirement-scenario.md

以上是关于TransmittableThreadLocal相关组件实用解读,及如何达到线程池中的线程复用,及使用在哪些线程数据传递场景?的主要内容,如果未能解决你的问题,请参考以下文章

阿里开源TransmittableThreadLocal(TTL)l的使用及原理解析

TransmittableThreadLocal相关组件实用解读,及如何达到线程池中的线程复用,及使用在哪些线程数据传递场景?

每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal

TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了

TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了

我居然被TransmittableThreadLocal框架作者评论了