[享學Netflix] 二十、Hystrix跨執行緒傳遞數據解決方案:HystrixRequestContext
- 2020 年 3 月 18 日
- 筆記
不要老想著做不順就放棄,哪個團隊都有問題,哪個團隊都有優點 程式碼下載地址:https://github.com/f641385712/netflix-learning
目錄
前言
說到執行緒間的數據傳遞,你肯定會聯想到ThreadLocal
。但若是跨執行緒傳遞數據呢?閱讀本文之前,我個人建議你已經能夠非常熟練的使用ThreadLocal
並了解其基本原理,至少看過下面兩篇文章表述的內容:
在Hystrix
里,它支援兩種隔離模式:執行緒池隔離和訊號量隔離。前者是默認選項:每個命令都在執行緒池裡隔離執行,因此必然會涉及到存在跨執行緒傳遞數據的問題,這是Hystrix需要解決的(訊號量隔離不存在此問題~)。
正文
關於執行緒內、執行緒間傳遞數據的進階三部曲:ThreadLocal -> InheritableThreadLocal -> TransmittableThreadLocal
,提供的能力越來越強。強兩者由JDK源生提供,最多能支援到父執行緒向子執行緒傳遞數據,但無法解決執行緒池執行問題。
TransmittableThreadLocal
是阿里巴巴開源的TTL工具,專用於解決執行緒間數據傳遞問題,支援執行緒池。但是很顯然,Hystrix不可能依賴於阿里巴巴的實現(阿里的年紀比它還小呢),所以它擁有自己的實現方式,核心API為:HystrixRequestContext/HystrixRequestVariableDefault/HystrixContextRunnable
。
HystrixRequestContext
它是請求級別的上下文,也就是說同一個請求內,前面放進去的內容,只要在請求生命周期內,任何地方均可以獲取到。
說明:這裡所指的請求級別,你可以理解為一個Http請求一樣,只是針對一個請求:請求內的任務可用多個執行緒或者使用執行緒池去處理,所以需要解決執行緒池內的數據獲取問題
public class HystrixRequestContext implements Closeable { // 利用ThreadLocal, 每個執行緒各有一份HystrixRequestContext // 當然,前提是調用了initializeContext()進行初始化 private static ThreadLocal<HystrixRequestContext> requestVariables = new ThreadLocal<>(); public static HystrixRequestContext initializeContext() { HystrixRequestContext state = new HystrixRequestContext(); requestVariables.set(state); // 每個執行緒都持有一份當前上下文state return state; } // 當然嘍,它也允許你自己重新設置綁定 // 為當前執行緒設置一個已存在的HystrixRequestContext public static void setContextOnCurrentThread(HystrixRequestContext state) { requestVariables.set(state); } // 得到當前執行緒的Hystrix請求上下文 從ThreadLocal里拿 public static HystrixRequestContext getContextForCurrentThread() { HystrixRequestContext context = requestVariables.get(); if (context != null && context.state != null) { return context; } else { return null; } } // 判斷當前執行緒的上下文是否已經初始化~~~~ 就是判斷get() == null而已嘛 public static boolean isCurrentThreadInitialized() { HystrixRequestContext context = requestVariables.get(); return context != null && context.state != null; } }
這段程式碼看似做的不多:給當前執行緒綁定一個HystrixRequestContext
上下文對象,當然嘍,這個上下文對象可以init初始化,也可來自於一個已經存在的上下文。
如何理解已經存在的上下文,比如父執行緒已經存在一個上下文,子執行緒想綁定時,是可以使用父上下文的,這樣父子不就共用一個上下文了麽?這樣數據就打通了嘛~
使用示例一:
@Test public void fun1() { HystrixRequestContext.initializeContext(); ... // 處理你的業務邏輯 HystrixRequestContext contextForCurrentThread = HystrixRequestContext.getContextForCurrentThread(); System.out.println(contextForCurrentThread.getClass()); // contextForCurrentThread.close(); contextForCurrentThread.shutdown(); }
運行程式控制台列印:
class com.netflix.hystrix.strategy.concurrency.HystrixRequestContext
是的,單獨使用HystrixRequestContext
的話,你就只能這麼簡單用啦。若配上Servlet的Filter,會更有意義些:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { chain.doFilter(request, response); } finally { context.shutdown(); } }
官方特彆強調:如果你調用了
initializeContext()
方法,請務必確保shutdown()
方法也會被調用,否則會造成可能的記憶體泄漏。
這樣子在Http請求的任意地方,便可輕鬆的拿到當前執行緒的HystrixRequestContext
對象,從而獲取相應數據。
但是,請繼續看如下示例:
@Test public void fun2() throws InterruptedException { HystrixRequestContext.initializeContext(); // 啟動子執行緒完成具體業務邏輯 new Thread(() -> { // 子執行緒里需要拿到請求上下文處理邏輯 HystrixRequestContext contextForCurrentThread = HystrixRequestContext.getContextForCurrentThread(); // ... // 處理業務邏輯 System.out.println("當前Hystrix請求上下文是:" + contextForCurrentThread); }).start(); HystrixRequestContext.getContextForCurrentThread().close(); TimeUnit.SECONDS.sleep(1); }
控制台列印:
當前Hystrix請求上下文是:null
我相信你在看了文首推薦的兩篇文章後,對這個結果並不會感到驚訝。有的人會說使用InheritableThreadLocal
能解決向子執行緒傳遞數據的問題,那麼問題是如果非同步任務交給執行緒池執行呢?難道你還得藉助阿里巴巴
的TTL來實現嗎?
提出疑問
我們知道Hystrix
默認的隔離方式是執行緒池:每個command/方法均會在執行緒池內執行,所以傳遞數據使用InheritableThreadLocal
是不能從根本上解決問題的,當然若你說藉助阿里巴巴
的TTL是可以解決問題,但讓堂堂Hystrix依賴阿里巴巴的庫,是不是也太不實際了呢?況且Hystrix的年齡可比TTL大多了~
因此,對於以上示例至少可以提出以下疑問:
- 光禿禿的在執行緒內傳遞一個
HystrixRequestContext
貌似沒有任何意義,上下文裡面可以裝數據嗎? - 對於子執行緒、執行緒池內獲取上下文
HystrixRequestContext
,Hystrix是如何解決的???
HystrixRequestContext
的Javadoc里有說,它還負責一件事:管理著HystrixRequestVariable
的生命周期(存儲和銷毀),而一個HystrixRequestVariable
介面便代表著請求變數:請求本地變數。
HystrixRequestVariable
它是一個介面,用於存儲一個變數,和上下文相關。
第一個問題:HystrixRequestContext
能裝東西嗎?那是必然呢,不然怎麼好意思叫上下文呢?
HystrixRequestContext: // 上下文所有的數據都使用這個Map來存儲 // state的訪問許可權是default級別:只能同包訪問 ConcurrentHashMap<HystrixRequestVariableDefault<?>, HystrixRequestVariableDefault.LazyInitializer<?>> state = new ConcurrentHashMap<>();
這是實際的存儲結構。每個執行緒綁定了一個HystrixRequestContext
,而每個HystrixRequestContext
有個Map結構存儲數據,key就HystrixRequestVariableDefault
。
HystrixRequestContext
並沒有提供直接訪問state
的相關方法,因此欄位並非private,它建議外部直接使用欄位進行訪問(添加數據、移除數據等)。而state
欄位的唯一訪問處是HystrixRequestVariableDefault
。
// 它的效果特別像ThreadLocal:用於傳遞請求級別的數據 // HystrixRequestVariableLifecycle方法:initialValue/shutdown(value) public interface HystrixRequestVariable<T> extends HystrixRequestVariableLifecycle<T> { // 獲取**當前請求**的值 public T get(); }
它有一個默認實現HystrixRequestVariableDefault
(可以認為是唯一實現)。
HystrixRequestVariableDefault
它用於儲存用戶請求級別的變數數據,而實際存儲依賴於HystrixRequestContext.state
,和請求上下文實例綁定,從而和執行緒綁定。
public class HystrixRequestVariableDefault<T> implements HystrixRequestVariable<T> { // 獲取和當前請求(注意:不是當前執行緒哦,因為自己可能是子執行緒或者) public T get() { ... // 必須確保HystrixRequestContext#init工作已做 否則拋出異常 ConcurrentHashMap<HystrixRequestVariableDefault<?>, LazyInitializer<?>> variableMap = HystrixRequestContext.getContextForCurrentThread().state; LazyInitializer<?> v = variableMap.get(this); if (v != null) { return (T) v.get(); } ... } // 初始值默認是null public T initialValue() { return null; } // 設置數據:直接調用put方法即可。使用new一個新的LazyInitializer裝起來 public void set(T value) { HystrixRequestContext.getContextForCurrentThread().state.put(this, new LazyInitializer<T>(this, value)); } public void remove() { ... } ... }
它看起來和ThreadLocal
效果類似,但是他倆有如下明顯區別:
- 使用它之前,必須調用
HystrixRequestContext#initializeContext
完成初始化才行(原因是HystrixRequestContext.state
才是底層存儲) - 它的清除動作是交給
HystrixRequestContext#shutdown
完成,所以請求結束後請你務必調用此方法
另外,它也說了,要想達到向子執行緒、執行緒池都可以傳遞數據的效果,你得使用Hystrix包裝後的HystrixContextCallable/HystrixContextRunnable
去初始化任務,才能達到預期的傳播效果。
HystrixContextRunnable
Hystrix 的思路是包裝Runnable,在執行實際的任務之前,先拿當前執行緒的HystrixRequestContext
初始化實際執行任務的執行緒的HystrixRequestContext
,這樣達到了同一個HystrixRequestContext
向下傳遞的目的了。
public class HystrixContextRunnable implements Runnable { private final Callable<Void> actual; // 父執行緒的上下文 private final HystrixRequestContext parentThreadState; public HystrixContextRunnable(Runnable actual) { this(HystrixPlugins.getInstance().getConcurrencyStrategy(), actual); } // 這裡使用了HystrixConcurrencyStrategy,它是一個SPI哦 // 你還可以通過該SPI,自定義你的執行機制,非常靠譜有木有 public HystrixContextRunnable(HystrixConcurrencyStrategy concurrencyStrategy, final Runnable actual) { this.actual = concurrencyStrategy.wrapCallable(() -> { actual.run(); // 實際執行的任務 return null; }); // 父執行緒奶你**構造時**所處在的執行緒 this.parentThreadState = HystrixRequestContext.getContextForCurrentThread(); } @Override public void run() { HystrixRequestContext existingState = HystrixRequestContext.getContextForCurrentThread(); try { HystrixRequestContext.setContextOnCurrentThread(parentThreadState); try { actual.call(); } catch (Exception e) { throw new RuntimeException(e); } } finally { HystrixRequestContext.setContextOnCurrentThread(existingState); } } }
處理思想和阿里巴巴的TTL如出一轍:執行前把父上下文設置進去,目標任務執行完成後,釋放父上下文。
另外:它還有個
HystrixContextCallable
用於包裝Callable,邏輯一毛一樣。
使用示例二:
@Test public void fun3() throws InterruptedException { HystrixRequestContext.initializeContext(); HystrixRequestContext mainContext = HystrixRequestContext.getContextForCurrentThread(); // 設置變數:讓其支援傳遞到子執行緒 or 執行緒池 NAME_VARIABLE.set("YoutBatman"); // 子執行緒的Runnable任務,必須使用`HystrixContextRunnable`才能得到上面設置的值哦 new Thread(new HystrixContextRunnable(() -> { HystrixRequestContext contextForCurrentThread = HystrixRequestContext.getContextForCurrentThread(); System.out.println(contextForCurrentThread == mainContext); System.out.println("當前執行緒綁定的變數值是:" + NAME_VARIABLE.get()); })).start(); TimeUnit.SECONDS.sleep(1); HystrixRequestContext.getContextForCurrentThread().close(); }
運行程式,控制台輸出:
true 當前執行緒綁定的變數值是:YoutBatman
請注意:這塊程式碼並沒有顯示的將 YourBatman 從 main執行緒傳遞到子執行緒,也沒有利用InheritableThreadLocal
哦。它的執行步驟可描述如下:
- main初始化
HystrixRequestContext
,並且和此Context上下文完成綁定 NAME_VARIABLE.set("YoutBatman")
設置變數值,請注意:此變數值是處在HystrixRequestContext
這個上下文里的哦,屬於main執行緒的內容- main執行緒初始化任務:使用的
HystrixContextRunnable
,所以該任務是和main執行緒的上下文綁定的 - 執行任務時,先用main執行緒的Context來初始化上下文(所以它綁定的上下文和main執行緒的是同一個上下文)
- 任務里使用
NAME_VARIABLE.get()
實際上是從main執行緒的上下文里拿數據,那必然可以取到呀
這就能解釋了:為何子執行緒(甚至是執行緒池裡的執行緒)都能拿到父執行緒里設置的變數了,因為是共用的同一個context上下文嘛。
HystrixConcurrencyStrategy通用方案解決跨執行緒傳值
HystrixConcurrencyStrategy: // 給我們一個機會:裝飾callable執行 public <T> Callable<T> wrapCallable(Callable<T> callable) { return callable; }
也就是說我們可以自定義該插件的實現,可以在目標任務執行前後加入自己的邏輯。下面舉一個常見例子,獲取是你可能遇到的坑
DemoController: @GetMapping("/demo") public String getDemo() { // 簡單的說:就是向Request域里放一個值 RequestContextHolder.currentRequestAttributes().setAttribute("name", "YourBatman", SCOPE_REQUEST); return demoService.getInfo(); } DemoService: public String getInfo() { Srting name = RequestContextHolder.currentRequestAttributes().getAttribute("name", SCOPE_REQUEST); return name; }
本來這一切是可以正常work的,但是如果現在突然說DemoService#getInfo()
要加入Hystrix熔斷降級:
DemoService: @HystrixCommand( ... ) public String getInfo() { Srting name = RequestContextHolder.currentRequestAttributes().getAttribute("name", SCOPE_REQUEST); return name; }
再次運行,what a fuck,返回null,直接導致邏輯錯誤,這非常致命。如果只有一個介面到時簡單,你可以採用方法參數傳遞方式fix,但若有多個呢???下面接針對性的介紹一種通用解決方案:自定義HystrixConcurrencyStrategy
/** * 此類能夠保證:RequestContext請求上下文,也就是RequestAttributes能夠在執行緒池裡自動有效 */ public class RequestContextHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy { // 這個時候還在主執行緒了,所以通過RequestContextHolder.getRequestAttributes()是能拿到上下文的 // 拿到後hold住,等到run執行的時候再綁定即可 @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(); } } } }
使用API方式註冊此插件:
HystrixPlugins.getInstance().registerConcurrencyStrategy(new RequestContextHystrixConcurrencyStrategy());
你也可以採用配置的方式,寫在plugin.properties/config.properties
里:
hystrix.plugin.HystrixConcurrencyStrategy.implementation=com.yourbatman.hystrix.RequestContextHystrixConcurrencyStrategy
這樣做後,媽媽就再也不用擔心你獲取不到請求上下文啦。另外本處只是以RequestContext
為例,當然還有像MDC傳值MDC.setContextMap(contextMap)
等等方案都是一樣的做。
總結
本文介紹了Netflix Hystrix
它自己的解決跨執行緒傳遞數據的結局方式,並且也介紹了HystrixConcurrencyStrategy
的擴展使用方式,該介面的擴展使用在Spring Cloud里也會有所體現。
總的來說,這篇文章內容和ThreadLocal
的思想是相同的,或者說和阿里巴巴的TTL更是相似。Hystrix
的隔離方式默認是執行緒池方式,所以加熔斷後是有何能影響你的正常邏輯的,請務必小心謹慎使用,特別在全鏈路追蹤時,很可能某些鏈路就失效了,影響你的全鏈路壓測邏輯…