Hystrix實現ThreadLocal上下文的傳遞

前言
Hystrix提供了基於信號量和線程兩種隔離模式,通過在Hystrix基礎章節中已經驗證過,通過@HystrixCommand註解的方法體將在新的線程中執行,這樣會帶來些什麼意想不到的意外呢,先來看一個示例:
1、定義一個webapi,通過RequestContextHolder設定一個當前線程的上下文:
@GetMapping(value = "/getServerInfo/{serviceName}")
public String getServer1Info(@PathVariable(value = "serviceName") String serviceName) {
    LOGGER.info("當前線程ID:" + Thread.currentThread().getId() + "當前線程Name" + Thread.currentThread().getName());
    RequestContextHolder.currentRequestAttributes().setAttribute("context", "main-thread-context", SCOPE_REQUEST);
    return consumeService.getServerInfo(serviceName);
}
2、在@HystrixCommand註解的方法中再次通過RequestContextHolder獲取當前上下文設定的value值:
@Override
@HystrixCommand(fallbackMethod = "getServerInfoFallback",
        commandProperties = {@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD")},
        commandKey = "cust2GetServerInfo",
        threadPoolKey = "cust2ThreadPool",
        groupKey = "cust2")
public String getServerInfo(String serviceName) {
    LOGGER.info(RibbonFilterContextHolder.getCurrentContext().get("TAG"));
    LOGGER.info(RequestContextHolder.currentRequestAttributes().getAttribute("context", SCOPE_REQUEST).toString());
    //如果是service1則需要添加http認證頭,service1暫時添加了認證機制;反之service2不需要認證直接發出請求即可
    if ("service1".equals(serviceName)) {
        HttpEntity<String> requestEntity = new HttpEntity<String>(getHeaders());
        ResponseEntity<String> responseEntity = restTemplate.exchange("http://" + serviceName + "/getServerInfo?userName=shuaishuai", HttpMethod.GET, requestEntity, String.class);
        return responseEntity.getBody();
    } else
        return restTemplate.getForObject("http://" + serviceName + "/getServerInfo?userName=shuaishuai", String.class);
}

public String getServerInfoFallback(String serviceName, Throwable e) {
    if (e != null) {
        LOGGER.error(e.getMessage());
    }
    return "Maybe the server named " + serviceName + " is not normal running";
}
3、啓動服務請求1中定義的API:

可以看到上圖中上下文的賦值與取值在不同的線程中執行,TAG信息被正常獲取,而RequestContextHolder設定的上線文信息獲取失敗,並進入回退方法並打印出了對應的異常信息,首先來看下爲何TAG信息被正常獲取,在RibbonFilterContextHolder中定義變量如下

而在RequestContextHolder中變量定義如下


其區別在於是採用ThreadLocalInheritableThreadLocal的差異,InheritableThreadLocal能夠在子線程中繼續傳播父線程的上線文,而ThreadLocal只能在保存在當前線程中,但事實上我們不可能所有的應用均採用InheritableThreadLocal,儘管他是一個不錯的選擇,但如何讓ThreadLocal也實現在Hystrix應用場景下實現線程上下文的傳播呢。這就是本章的重點了。


本章概要
1、資料搜索;
2、源碼分析;
3、擴展HystrixConcurrencyStrategy解決前言中的意外;
4、提高HystrixConcurrencyStrategy包裝擴展性;

資料搜索
既然遇到了問題,就到springcloud的官方文檔先檢索下,找到如下對應的描述

紅色框部分主要意思是,我們可以聲明一個定製化的HystrixConcurrencyStrategy實例,並通過HystrixPlugins註冊。先找到HystrixConcurrencyStrategy類,其有下面一段類註釋
For example, every {@link Callable} executed by {@link HystrixCommand} will call {@link #wrapCallable(Callable)} to give a chance for custom implementations to decorate the {@link Callable} with additional behavior.
@HystrixCommand註解的方法,其執行源Callable可以通過wrapCallable方法進行定製化裝飾,加入附加的行爲,繼續來看看wrapCallable方法的定義

其同樣提供了非常詳細的註釋,該方法提供了在方法被執行前進行裝飾的機會,可以用來複制線程狀態等附加行爲,這個貌似就是我們需要的,很合意。
同樣在Hystrix官方文檔提供了更加詳細的說明(https://github.com/Netflix/Hystrix/wiki/Plugins#concurrency-strategy),Concurrency Strategy作爲了Plugin的一種類別,描述如下

可以看到紅色框中的重點描述,其已經說了非常明確,可以從父線程複製線程狀態至子線程。自定義的Plugin如何被HystrixCommand應用呢,繼續查看官方的描述

其提供了HystrixPlugins幫助我們註冊自定義的Plugin,除了我們本章節重點關注的Concurrency Strategy類別plugin,還有如下類別以及對應的抽象實現
類別
抽象實現
Event Notifier
Metrics Publisher
Properties Strategy
Concurrency Strategy
Command Execution Hook


源碼分析
在springcloud中還有如下一段話

既然提高了定製化的實現,不如來看看官方已經提供了哪些默認實現

首先來看看HystrixConcurrencyStrategyDefault

很精簡的一段代碼,並沒有任何方法重寫,其作爲了一個標準提供默認實現。繼續來看看SecurityContextConcurrencyStrategy實現,直接找到wrapCallable方法

其對Callabe進行了二次包裝,繼續跟進來看看DelegatingSecurityContextCallable的定義

其主要實現均在call方法中,紅色框中標出了重點,在調用call方法前,我們可以將當前上下文信息放入SecurityContextHolder中,在執行完成後清空SecurityContextHolder對應的設置。再來看看SecurityContextConcurrencyStrategy是如何被應用的,在HystrixSecurityAutoConfiguration中有如下代碼段

在啓動註冊配置過程中機會通過HystrixPlugins註冊當前擴展的HystrixConcurrencyStrategy實現。

小節:自定義擴展類實現Callable接口,並傳入當前Callable變量delegate,在delegate執行call方法前後進行線程上線文的操作即可實現線程狀態在父線程與子線程間的傳播。

擴展HystrixConcurrencyStrategy解決前言中的意外
通過源碼部分的解讀,基本瞭解springcloud是如何實現擴展的,又是如何被應用的,照葫蘆畫瓢下。

1、定義一個RequestContextHystrixConcurrencyStrategy實現HystrixConcurrencyStrategy接口,並重寫其wrapCallable方法:
public class RequestContextHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {

    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        return new RequestAttributeAwareCallable<>(callable, RequestContextHolder.getRequestAttributes());
    }

    static class RequestAttributeAwareCallable<T> implements Callable<T> {

        private final Callable<T> delegate;
        private final RequestAttributes requestAttributes;

        public RequestAttributeAwareCallable(Callable<T> callable, RequestAttributes requestAttributes) {
            this.delegate = callable;
            this.requestAttributes = requestAttributes;
        }

        @Override
        public T call() throws Exception {
            try {
                RequestContextHolder.setRequestAttributes(requestAttributes);
                return delegate.call();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        }
    }
}
其中定義RequestAttributeAwareCallable裝飾類,通過構造函數傳入當前待執行Callable代理和當前待傳播的RequestAttributes值,並在delegatecall方法執行前對RequestContextHolderRequestAttributes賦值,在finally塊中重置。

2、同樣在任意配置類中添加如下代碼段即可,通過HystrixPlugins註冊RequestContextHystrixConcurrencyStrategy
@PostConstruct
public void init() {
    HystrixPlugins.getInstance().registerConcurrencyStrategy(new RequestContextHystrixConcurrencyStrategy());
}
3、啓動服務驗證,子線程取值成功:


小節:以上參考SecurityContextConcurrencyStrategy的實現,完成了Hystrix中RequestContextHolder上下文信息傳播。

提高HystrixConcurrencyStrategy包裝擴展性
上一個小節介紹瞭如果在Hystrix線程隔離場景下實現ThreadLocal定義的上下文傳播,根據示例,在實際應用過程中如果我們有多個類似RequestContextHystrixConcurrencyStrategy策略,需要將每個自定義HystrixConcurrencyStrategy示例註冊至HystrixPlugins中,這在擴展性方面顯然是缺失的,借鑑spring的實踐,我們可以定義對Callable的包裝接口HystrixCallableWrapper,根據實際的業務只需要對HystrixCallableWrapper進行實現,並註冊對應的實現bean即可。具體實現如下:

1、定義用於包裝hystrix中Callable實例的接口:
public interface HystrixCallableWrapper {

    /**
     * 包裝Callable實例
     *
     * @param callable 待包裝實例
     * @param <T>      返回類型
     * @return 包裝後的實例
     */
    <T> Callable<T> wrap(Callable<T> callable);

}

2、通過之前的源碼閱讀與實踐,基本已經發現實現線程上線文傳播的核心在於對Callable進行包裝,通過多次對Callable包裝即實現了一個鏈式包裝過程,如下擴展HystrixConcurrencyStrategy接口實現RequestContextHystrixConcurrencyStrategy,其中定義CallableWrapperChain類對所有注入的HystrixCallableWrapper包裝實現進行裝配:
public class RequestContextHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
    private final Collection<HystrixCallableWrapper> wrappers;

    public RequestContextHystrixConcurrencyStrategy(Collection<HystrixCallableWrapper> wrappers) {
        this.wrappers = wrappers;
    }

    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        return new CallableWrapperChain(callable, wrappers.iterator()).wrapCallable();
    }

    private static class CallableWrapperChain<T> {

        private final Callable<T> callable;

        private final Iterator<HystrixCallableWrapper> wrappers;

        CallableWrapperChain(Callable<T> callable, Iterator<HystrixCallableWrapper> wrappers) {
            this.callable = callable;
            this.wrappers = wrappers;
        }

        Callable<T> wrapCallable() {
            Callable<T> delegate = callable;
            while (wrappers.hasNext()) {
                delegate = wrappers.next().wrap(delegate);
            }
            return delegate;
        }
    }
}
3、實現HystrixCallableWrapper接口,定義一個包裝RequestContextHolder上下文處理的實現類:
public final class RequestAttributeAwareCallableWrapper implements HystrixCallableWrapper {
    @Override
    public <T> Callable<T> wrap(Callable<T> callable) {
        return new RequestAttributeAwareCallable(callable, RequestContextHolder.getRequestAttributes());
    }

    static class RequestAttributeAwareCallable<T> implements Callable<T> {

        private final Callable<T> delegate;
        private final RequestAttributes requestAttributes;

        RequestAttributeAwareCallable(Callable<T> callable, RequestAttributes requestAttributes) {
            this.delegate = callable;
            this.requestAttributes = requestAttributes;
        }

        @Override
        public T call() throws Exception {
            try {
                RequestContextHolder.setRequestAttributes(requestAttributes);
                return delegate.call();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        }
    }
}
4、實現HystrixCallableWrapper接口,定義一個包裝Mdc日誌處理上下文的實現類:
public class MdcAwareCallableWrapper implements HystrixCallableWrapper {
    @Override
    public <T> Callable<T> wrap(Callable<T> callable) {
        return new MdcAwareCallable<>(callable, MDC.getCopyOfContextMap());
    }

    private class MdcAwareCallable<T> implements Callable<T> {

        private final Callable<T> delegate;

        private final Map<String, String> contextMap;

        public MdcAwareCallable(Callable<T> callable, Map<String, String> contextMap) {
            this.delegate = callable;
            this.contextMap = contextMap != null ? contextMap : new HashMap();
        }

        @Override
        public T call() throws Exception {
            try {
                MDC.setContextMap(contextMap);
                return delegate.call();
            } finally {
                MDC.clear();
            }
        }
    }
}
5、最後通過在Configuration配置類中註冊如下HystrixCallableWrapper 實現類的bean實例,並通過HystrixPlugins註冊擴展包裝實現:
@Bean
public HystrixCallableWrapper requestAttributeAwareCallableWrapper() {
    return new RequestAttributeAwareCallableWrapper();
}

@Bean
public HystrixCallableWrapper mdcAwareCallableWrapper(){
    return new MdcAwareCallableWrapper();
}

@Autowired(required = false)
private List<HystrixCallableWrapper> wrappers = new ArrayList<>();

@PostConstruct
public void init() {
    HystrixPlugins.getInstance().registerConcurrencyStrategy(new RequestContextHystrixConcurrencyStrategy(wrappers));
}

總結
本章從官方網站與源碼出發,逐步實現了hystrix中如何進行線程上下文的傳播。同時爲了更好的擴展性,提供了基於自定義接口並注入實現的方式。

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