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實現中,只是看上去比較複雜。

參考資料


歡迎關注陳同學的公衆號,一起學習,一起成長

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章