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中所有的TransmittableThreadLocal, copiedRef中不存在,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级缓存有一下好处:
- 避免重复执行高成本操作,提升性能。
- 避免不同Session之间的数据污染。
https://github.com/alibaba/transmittable-thread-local/blob/master/docs/requirement-scenario.md
以上是关于TransmittableThreadLocal相关组件实用解读,及如何达到线程池中的线程复用,及使用在哪些线程数据传递场景?的主要内容,如果未能解决你的问题,请参考以下文章
阿里开源TransmittableThreadLocal(TTL)l的使用及原理解析
TransmittableThreadLocal相关组件实用解读,及如何达到线程池中的线程复用,及使用在哪些线程数据传递场景?
每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal
TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了