Spring Cloud 之 Hystrix 跨线程传递数据

欢迎访问陈同学博客原文

本文以一个技术场景来学习 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有两个属性,threadLocalsinheritableThreadLocals,结构完全一样。当使用 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实现中,只是看上去比较复杂。

参考资料


欢迎关注陈同学的公众号,一起学习,一起成长

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值