陈同学
微服务
Accelerator
About
# Spring Cloud 之 Hystrix 跨线程传递数据 本文以一个技术场景来学习 Hystrix 跨线程传递数据的知识。将先简述ThreadLocal、InheritableThreadLocal跨父子线程传递数据,再进入主题。 基于Spring Boot **2.0.6.RELEASE**, Spring Cloud **Finchley.SR1**。 ## 技术场景 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/10/27/ce4ccc2e70ec43b88d383d5985d87127.png) **服务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分别代表这两个对象。 ```java 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")** ; 数据存储情况如下: <img src="https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/10/27/e226c99af89d4fd78e9af0e31f64a0f4.png" width="60%"> 每个线程都有个ThreadLocalMap类型的属性 **threadLocals**,它用于存储线程私有数据,ThreadLocal对象只是充当检索数据的Key,本身不存储任何信息。 再看下 **InheritableThreadLocal** 怎么在父子线程之间传递数据,如下例子: ```java static final ThreadLocal<String> TOKEN = new InheritableThreadLocal<>(); ``` Thread有两个属性,**threadLocals** 和 **inheritableThreadLocals**,结构完全一样。当使用 **InheritableThreadLocal**时,数据存储在inheritableThreadLocals中,否则存储在threadLocals。 ```java ThreadLocalMap threadLocals = null; ThreadLocalMap inheritableThreadLocals = null; ``` **在创建子线程时,会将父线程的inheritableThreadLocals拷贝到子线程,从而达到跨线程传递数据的目的。** 下面是Thread构造器上初始化的一段代码: ```java private void init(ThreadGroup g, Runnable target, String name ...) { ... if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); ... } ``` ## Hystrix如何跨线程传递数据 上面的父子线程传递数据,子线程可以访问父线程数据。但从tomcat线程向hystrix线程池线程传递数据情况有所不同,并不清楚任务最终由hystrix线程池中的哪个线程执行,而且两种线程的关系八杆子打不着。 Hystrix提供了如下方案来解决上述问题(我将代码全写在了一起,看注释结合下面的图,否则容易晕): ```java // 用HystrixRequestVariableDefault做Key检索数据, 类比ThreadLocal static final HystrixRequestVariableDefault<String> TOKEN = new HystrixRequestVariableDefault<>(); ``` 在某个 ServletFilter 进行处理: ```java // 在当前线程初始化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中有两个非常重要的属性: ```java // requestVariables, 每个线程存储自己的 HystrixRequestContext private static ThreadLocal<HystrixRequestContext> requestVariables = new ThreadLocal<HystrixRequestContext>(); // state属性,V是我自己简化的, 表示存储的值 ConcurrentHashMap<HystrixRequestVariableDefault<?>, V> state = new ... ``` 画个图来翻译下上面的代码: ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/10/27/d9821389c0f145daacae038af9ced1cc.png) * 先创建静态变量 HystrixRequestVariableDefault#**1024** 和 ThreadLocal#**1025** ```java 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"> ```java TOKEN.set("I am a token") ``` * 在执行 TOKEN.get() 时,先通过ThreadLocal对象作为Key检索到线程中存储的HystrixRequestContext,然后通过HystrixRequestVariableDefault对象作为Key从HystrixRequestContext.state中获取对应的值。 上面其实没解决跨线程传递数据问题,绕了一圈,用的还是ThreadLocal,并没有线程间数据传递的过程。 调试后找到跨线程传递数据的地方,如下截图,展示了从Feign发起调用,到任务被Hystrix线程执行的过程: ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/10/27/4f1860e8f6234f14ba24dfd2a56f7eef.png) 再看下图,HystrixContexSchedulerAction。 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/10/27/1c5031e58cc349b6a0704bc119a5852e.png) 它是在tomcat工作线程中创建的,因此可以拿到token,并将token存在了HystrixContexSchedulerAction对象中。 ```java this.parentThreadState = HystrixRequestContext.getContextForCurrentThread(); ``` 当Hystrix线程执行这个任务时,任务本身就存储了token,在执行任务前,利用下面的代码把token存储到hystrix工作线程。需要注意的是:parentThreadState是一个HystrixRequestContext类型的引用,也就是说tomcat工作线程在销毁HystrixRequestContext时,Hystrix线程中存储的数据同样也就销毁了。 ```java HystrixRequestContext.setContextOnCurrentThread(parentThreadState); ``` 上面整个过程比较麻烦,我在调试时也找了很久才找到跨线程传递数据的地方。 ## 跨数据传递的简单例子 通过Task对象本身来跨线程传递数据,Hystrix简化后其实就是下面的样子。 ```java 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跨线程传递问题](https://www.jianshu.com/p/c60fe209a799) from 占小狼 * [Hystrix Isolation](https://github.com/Netflix/Hystrix/wiki/How-it-Works#Isolation) from Hystrix Github Wiki
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
Spring Cloud 之 Hystrix 跨线程传递数据
文章链接:
https://chenyongjun.vip/articles/83
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力