欢迎访问陈同学博客原文
本文以一个技术场景来学习 Hystrix 跨线程传递数据的知识。将先简述ThreadLocal、InheritableThreadLocal跨父子线程传递数据,再进入主题。
基于Spring Boot 2.0.6.RELEASE, Spring Cloud Finchley.SR1。
技术场景
服务A 通过 Feign + Hystrix 调用服务B,服务间调用时需传递 JWT Token,希望在Feign发起的所有请求中都自动加上token以在各服务中传递环境信息。JWT Token 一是用于确保访问的安全,二是存储用户context信息。
请求进入服务A时,首先通过 Filter 验证 JWT token,接着把 token 存到当前线程(tomcat工作线程)。调用 Service B时,tomcat工作线程将把任务交给 Hystrix 线程池处理,这是本文主题:如何将token从tomcat工作线程传递到hystrix线程池线程?
Hystrix 线程池
先提一下Hystrix线程池。当 Hystrix 隔离策略为线程池时,在上述场景中:
假设服务B的应用名为 Service-B,在服务A中,Hystrix 会专门创建线程池用于执行对 Service-B的调用,上图中的 hystrix-Service-B-n 就是其线程池中的线程。
通过这种方式,应用A对依赖进行了隔离。默认对每个依赖的服务分配10个线程。隔离的好处例举几个:
- 请求太多,10个线程处理不过来?=> 那就拒绝请求,保护应用
- 依赖服务调用出错?失败?=> 那就降级 或 快速失败
- 依赖服务调用延时?=> 想耍流盲,占用资源不放,那就利用超时监控机制,干掉流氓任务
- …
父子线程如何传递数据
跨线程传递数据的场景有:父子线程和其他任意线程之间。
先看下ThreadLocal,定义两个ThreadLocal类型的对象:TOKEN 和 USER,假定用1024、1025分别代表这两个对象。
static final ThreadLocal<String> TOKEN = new ThreadLocal<>(); // 1024
static final ThreadLocal<String> USER = new ThreadLocal<>(); // 1025
在Thread1中执行: TOKEN.set(“1”); USER.set(“1”) ;
在Thread2中执行: TOKEN.set(“2”); USER.set(“2”) ;
数据存储情况如下:
每个线程都有个ThreadLocalMap类型的属性 threadLocals,它用于存储线程私有数据,ThreadLocal对象只是充当检索数据的Key,本身不存储任何信息。
再看下 InheritableThreadLocal 怎么在父子线程之间传递数据,如下例子:
static final ThreadLocal<String> TOKEN = new InheritableThreadLocal<>();
Thread有两个属性,threadLocals 和 inheritableThreadLocals,结构完全一样。当使用 InheritableThreadLocal时,数据存储在inheritableThreadLocals中,否则存储在threadLocals。
ThreadLocalMap threadLocals = null;
ThreadLocalMap inheritableThreadLocals = null;
在创建子线程时,会将父线程的inheritableThreadLocals拷贝到子线程,从而达到跨线程传递数据的目的。
下面是Thread构造器上初始化的一段代码:
private void init(ThreadGroup g, Runnable target, String name ...) {
...
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}
Hystrix如何跨线程传递数据
上面的父子线程传递数据,子线程可以访问父线程数据。但从tomcat线程向hystrix线程池线程传递数据情况有所不同,并不清楚任务最终由hystrix线程池中的哪个线程执行,而且两种线程的关系八杆子打不着。
Hystrix提供了如下方案来解决上述问题(我将代码全写在了一起,看注释结合下面的图,否则容易晕):
// 用HystrixRequestVariableDefault做Key检索数据, 类比ThreadLocal
static final HystrixRequestVariableDefault<String> TOKEN = new HystrixRequestVariableDefault<>();
在某个 ServletFilter 进行处理:
// 在当前线程初始化HystrixRequestContext, 并设置token
HystrixRequestContext context = null;
if (!HystrixRequestContext.isCurrentThreadInitialized()) {
HystrixRequestContext.initializeContext();
}
try {
TOKEN.set("I am a token");
chain.doFilter(request, response);
} finally {
// 销毁当前线程HystrixRequestContext
if (HystrixRequestContext.isCurrentThreadInitialized()) {
HystrixRequestContext.getContextForCurrentThread().shutdown();
}
}
HystrixRequestContext 表示Request级别的context信息,在线程池环境下,用法和ThreadLocal差不多。
HystrixRequestContext中有两个非常重要的属性:
// requestVariables, 每个线程存储自己的 HystrixRequestContext
private static ThreadLocal<HystrixRequestContext> requestVariables = new ThreadLocal<HystrixRequestContext>();
// state属性,V是我自己简化的, 表示存储的值
ConcurrentHashMap<HystrixRequestVariableDefault<?>, V> state = new ...
画个图来翻译下上面的代码:
- 先创建静态变量 HystrixRequestVariableDefault#1024 和 ThreadLocal#1025
static final HystrixRequestVariableDefault<String> TOKEN = new HystrixRequestVariableDefault<>();
private static ThreadLocal<HystrixRequestContext> requestVariables = new ThreadLocal<HystrixRequestContext>();
- 在当前线程(tomcat的线程)通过1025这个Key找到对应的HystrixRequestContext,然后往HystrixRequestContext的state属性中put<1024, “I am a token”>
TOKEN.set("I am a token")
- 在执行 TOKEN.get() 时,先通过ThreadLocal对象作为Key检索到线程中存储的HystrixRequestContext,然后通过HystrixRequestVariableDefault对象作为Key从HystrixRequestContext.state中获取对应的值。
上面其实没解决跨线程传递数据问题,绕了一圈,用的还是ThreadLocal,并没有线程间数据传递的过程。
调试后找到跨线程传递数据的地方,如下截图,展示了从Feign发起调用,到任务被Hystrix线程执行的过程:
再看下图,HystrixContexSchedulerAction。
它是在tomcat工作线程中创建的,因此可以拿到token,并将token存在了HystrixContexSchedulerAction对象中。
this.parentThreadState = HystrixRequestContext.getContextForCurrentThread();
当Hystrix线程执行这个任务时,任务本身就存储了token,在执行任务前,利用下面的代码把token存储到hystrix工作线程。需要注意的是:parentThreadState是一个HystrixRequestContext类型的引用,也就是说tomcat工作线程在销毁HystrixRequestContext时,Hystrix线程中存储的数据同样也就销毁了。
HystrixRequestContext.setContextOnCurrentThread(parentThreadState);
上面整个过程比较麻烦,我在调试时也找了很久才找到跨线程传递数据的地方。
跨数据传递的简单例子
通过Task对象本身来跨线程传递数据,Hystrix简化后其实就是下面的样子。
public class Demo {
private static ThreadLocal<String> TOKEN = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
TOKEN.set("1024");
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 在当前线程创建任务, 通过任务把token传递到其他线程
executorService.submit(new Task("1024"));
Thread.sleep(1000);
}
// 通过任务传递token
static class Task implements Runnable {
private String token;
public Task(String token) {
this.token = token;
}
@Override
public void run() {
// 从Task中获取token并设置到当前线程
TOKEN.set(this.token);
}
}
}
小结
Runnable 和 Callable 都是可以被线程执行的Task,无论最终由哪个线程来执行,在各个线程间传递数据比较好的方式依然是通过任务本身。跨线程传递数据只是Hystrix中的一个小细节,实现过程也夹杂在复杂的Hystrix实现中,只是看上去比较复杂。
参考资料
- Hystrix系列之ThreadLocal跨线程传递问题 from 占小狼
- Hystrix Isolation from Hystrix Github Wiki
欢迎关注陈同学的公众号,一起学习,一起成长