Hystrix 實現資源隔離的“兩把利器”
小型電商網站的商品詳情頁系統架構
小型電商網站的頁面展示采用頁面全量靜態化的思想。數據庫中存放了所有的商品信息,頁面靜態化系統,將數據填充進靜態模板中,形成靜態化頁面,推入 Nginx 服務器。用戶瀏覽網站頁面時,取用一個已經靜態化好的 html 頁面,直接返回回去,不涉及任何的業務邏輯處理。

下面是頁面模板的簡單 Demo 。
- <html> <body> 商品名稱:#{productName}<br> 商品價格:#{productPrice}<br> 商品描述:#{productDesc} </body></html>
這樣做,好處在于,用戶每次瀏覽一個頁面,不需要進行任何的跟數據庫的交互邏輯,也不需要執行任何的代碼,直接返回一個 html 頁面就可以了,速度和性能非常高。
對于小網站,頁面很少,很實用,非常簡單,Java 中可以使用 velocity、freemarker、thymeleaf 等等,然后做個 cms 頁面內容管理系統,模板變更的時候,點擊按鈕或者系統自動化重新進行全量渲染。
壞處在于,僅僅適用于一些小型的網站,比如頁面的規模在幾十到幾萬不等。對于一些大型的電商網站,億級數量的頁面,你說你每次頁面模板修改了,都需要將這么多頁面全量靜態化,靠譜嗎?每次渲染花個好幾天時間,那你整個網站就廢掉了。
大型電商網站的商品詳情頁系統架構
大型電商網站商品詳情頁的系統設計中,當商品數據發生變更時,會將變更消息壓入 MQ 消息隊列中。緩存服務從消息隊列中消費這條消息時,感知到有數據發生變更,便通過調用數據服務接口,獲取變更后的數據,然后將整合好的數據推送至 redis 中。Nginx 本地緩存的數據是有一定的時間期限的,比如說 10 分鐘,當數據過期之后,它就會從 redis 獲取到最新的緩存數據,并且緩存到自己本地。
用戶瀏覽網頁時,動態將 Nginx 本地數據渲染到本地 html 模板并返回給用戶。

雖然沒有直接返回 html 頁面那么快,但是因為數據在本地緩存,所以也很快,其實耗費的也就是動態渲染一個 html 頁面的性能。如果 html 模板發生了變更,不需要將所有的頁面重新靜態化,也不需要發送請求,沒有網絡請求的開銷,直接將數據渲染進最新的 html 頁面模板后響應即可。
在這種架構下,我們需要保證系統的高可用性。
如果系統訪問量很高,Nginx 本地緩存過期失效了,redis 中的緩存也被 LRU 算法給清理掉了,那么會有較高的訪問量,從緩存服務調用商品服務。但如果此時商品服務的接口發生故障,調用出現了延時,緩存服務全部的線程都被這個調用商品服務接口給耗盡了,每個線程去調用商品服務接口的時候,都會卡住很長時間,后面大量的請求過來都會卡在那兒,此時緩存服務沒有足夠的線程去調用其它一些服務的接口,從而導致整個大量的商品詳情頁無法正常顯示。
這其實就是一個商品接口服務故障導致緩存服務資源耗盡的現象。
基于 Hystrix 線程池技術
上文提到,如果從 Nginx 開始,緩存都失效了,Nginx 會直接通過緩存服務調用商品服務獲取最新商品數據(我們基于電商項目做個討論),有可能出現調用延時而把緩存服務資源耗盡的情況。這里,我們就來說說,怎么通過 Hystrix 線程池技術實現資源隔離。
資源隔離,就是說,你如果要把對某一個依賴服務的所有調用請求,全部隔離在同一份資源池內,不會去用其它資源了,這就叫資源隔離。哪怕對這個依賴服務,比如說商品服務,現在同時發起的調用量已經到了 1000,但是分配給商品服務線程池內就 10 個線程,最多就只會用這 10 個線程去執行。不會因為對商品服務調用的延遲,將 Tomcat 內部所有的線程資源全部耗盡。
Hystrix 進行資源隔離,其實是提供了一個抽象,叫做 Command。這也是 Hystrix 最最基本的資源隔離技術。
利用 HystrixCommand 獲取單條數據
我們通過將調用商品服務的操作封裝在 HystrixCommand 中,限定一個 key,比如下面的 GetProductInfoCommandGroup,在這里我們可以簡單認為這是一個線程池,每次調用商品服務,就只會用該線程池中的資源,不會再去用其它線程資源了。
- public class GetProductInfoCommand extends HystrixCommand<ProductInfo> {
- private Long productId;
- public GetProductInfoCommand(Long productId) { super(HystrixCommandGroupKey.Factory.asKey("GetProductInfoCommandGroup")); this.productId = productId; }
- @Override protected ProductInfo run() { String url = "http://localhost:8081/getProductInfo?productId=" + productId; // 調用商品服務接口 String response = HttpClientUtils.sendGetRequest(url); return JSONObject.parseObject(response, ProductInfo.class); }}
我們在緩存服務接口中,根據 productId 創建 Command 并執行,獲取到商品數據。
- @RequestMapping("/getProductInfo")@ResponseBodypublic String getProductInfo(Long productId) { HystrixCommand<ProductInfo> getProductInfoCommand = new GetProductInfoCommand(productId);
- // 通過command執行,獲取最新商品數據 ProductInfo productInfo = getProductInfoCommand.execute(); System.out.println(productInfo); return "success";}
上面執行的是 execute() 方法,其實是同步的。也可以對 command 調用 queue() 方法,它僅僅是將 command 放入線程池的一個等待隊列,就立即返回,拿到一個 Future 對象,后面可以繼續做其它一些事情,然后過一段時間對 Future 調用 get() 方法獲取數據。這是異步的。
利用 HystrixObservableCommand 批量獲取數據
只要是獲取商品數據,全部都綁定到同一個線程池里面去,我們通過 HystrixObservableCommand 的一個線程去執行,而在這個線程里面,批量把多個 productId 的 productInfo 拉回來。
- public class GetProductInfosCommand extends HystrixObservableCommand<ProductInfo> {
- private String[] productIds;
- public GetProductInfosCommand(String[] productIds) { // 還是綁定在同一個線程池 super(HystrixCommandGroupKey.Factory.asKey("GetProductInfoGroup")); this.productIds = productIds; }
- @Override protected Observable<ProductInfo> construct() { return Observable.unsafeCreate((Observable.OnSubscribe<ProductInfo>) subscriber -> {
- for (String productId : productIds) { // 批量獲取商品數據 String url = "http://localhost:8081/getProductInfo?productId=" + productId; String response = HttpClientUtils.sendGetRequest(url); ProductInfo productInfo = JSONObject.parseObject(response, ProductInfo.class); subscriber.onNext(productInfo); } subscriber.onCompleted();
- }).subscribeOn(Schedulers.io()); }}
在緩存服務接口中,根據傳來的 id 列表,比如是以 , 分隔的 id 串,通過上面的 HystrixObservableCommand,執行 Hystrix 的一些 API 方法,獲取到所有商品數據。
- public String getProductInfos(String productIds) { String[] productIdArray = productIds.split(","); HystrixObservableCommand<ProductInfo> getProductInfosCommand = new GetProductInfosCommand(productIdArray); Observable<ProductInfo> observable = getProductInfosCommand.observe();
- observable.subscribe(new Observer<ProductInfo>() { @Override public void onCompleted() { System.out.println("獲取完了所有的商品數據"); }
- @Override public void onError(Throwable e) { e.printStackTrace(); }
- /** * 獲取完一條數據,就回調一次這個方法 * @param productInfo */ @Override public void onNext(ProductInfo productInfo) { System.out.println(productInfo); } }); return "success";}
我們回過頭來,看看 Hystrix 線程池技術是如何實現資源隔離的。

從 Nginx 開始,緩存都失效了,那么 Nginx 通過緩存服務去調用商品服務。緩存服務默認的線程大小是 10 個,最多就只有 10 個線程去調用商品服務的接口。即使商品服務接口故障了,最多就只有 10 個線程會 hang 死在調用商品服務接口的路上,緩存服務的 Tomcat 內其它的線程還是可以用來調用其它的服務,干其它的事情。
基于 Hystrix 信號量
Hystrix 里面核心的一項功能,其實就是所謂的資源隔離,要解決的最最核心的問題,就是將多個依賴服務的調用分別隔離到各自的資源池內。避免說對某一個依賴服務的調用,因為依賴服務的接口調用的延遲或者失敗,導致服務所有的線程資源全部耗費在這個服務的接口調用上。一旦說某個服務的線程資源全部耗盡的話,就可能導致服務崩潰,甚至說這種故障會不斷蔓延。
Hystrix 實現資源隔離,主要有兩種技術:
•線程池•信號量
默認情況下,Hystrix 使用線程池模式。
前面已經說過線程池技術了,這一小節就來說說信號量機制實現資源隔離,以及這兩種技術的區別與具體應用場景。
信號量機制
信號量的資源隔離只是起到一個開關的作用,比如,服務 A 的信號量大小為 10,那么就是說它同時只允許有 10 個 tomcat 線程來訪問服務 A,其它的請求都會被拒絕,從而達到資源隔離和限流保護的作用。

線程池與信號量區別
線程池隔離技術,并不是說去控制類似 tomcat 這種 web 容器的線程。更加嚴格的意義上來說,Hystrix 的線程池隔離技術,控制的是 tomcat 線程的執行。Hystrix 線程池滿后,會確保說,tomcat 的線程不會因為依賴服務的接口調用延遲或故障而被 hang 住,tomcat 其它的線程不會卡死,可以快速返回,然后支撐其它的事情。
線程池隔離技術,是用 Hystrix 自己的線程去執行調用;而信號量隔離技術,是直接讓 tomcat 線程去調用依賴服務。信號量隔離,只是一道關卡,信號量有多少,就允許多少個 tomcat 線程通過它,然后去執行。

適用場景:
•線程池技術,適合絕大多數場景,比如說我們對依賴服務的網絡請求的調用和訪問、需要對調用的 timeout 進行控制(捕捉 timeout 超時異常)。•信號量技術,適合說你的訪問不是對外部依賴的訪問,而是對內部的一些比較復雜的業務邏輯的訪問,并且系統內部的代碼,其實不涉及任何的網絡請求,那么只要做信號量的普通限流就可以了,因為不需要去捕獲 timeout 類似的問題。
信號量簡單 Demo
業務背景里,比較適合信號量的是什么場景呢?
比如說,我們一般來說,緩存服務,可能會將一些量特別少、訪問又特別頻繁的數據,放在自己的純內存中。
舉個栗子。一般我們在獲取到商品數據之后,都要去獲取商品是屬于哪個地理位置、省、市、賣家等,可能在自己的純內存中,比如就一個 Map 去獲取。對于這種直接訪問本地內存的邏輯,比較適合用信號量做一下簡單的隔離。
優點在于,不用自己管理線程池啦,不用 care timeout 超時啦,也不需要進行線程的上下文切換啦。信號量做隔離的話,性能相對來說會高一些。
假如這是本地緩存,我們可以通過 cityId,拿到 cityName。
- public class LocationCache { private static Map<Long, String> cityMap = new HashMap<>();
- static { cityMap.put(1L, "北京"); }
- /** * 通過cityId 獲取 cityName * * @param cityId 城市id * @return 城市名 */ public static String getCityName(Long cityId) { return cityMap.get(cityId); }}
寫一個 GetCityNameCommand,策略設置為信號量。run() 方法中獲取本地緩存。我們目的就是對獲取本地緩存的代碼進行資源隔離。
- public class GetCityNameCommand extends HystrixCommand<String> {
- private Long cityId;
- public GetCityNameCommand(Long cityId) { // 設置信號量隔離策略 super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetCityNameGroup")) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)));
- this.cityId = cityId; }
- @Override protected String run() { // 需要進行信號量隔離的代碼 return LocationCache.getCityName(cityId); }}
在接口層,通過創建 GetCityNameCommand,傳入 cityId,執行 execute() 方法,那么獲取本地 cityName 緩存的代碼將會進行信號量的資源隔離。
- @RequestMapping("/getProductInfo")@ResponseBodypublic String getProductInfo(Long productId) { HystrixCommand<ProductInfo> getProductInfoCommand = new GetProductInfoCommand(productId);
- // 通過command執行,獲取最新商品數據 ProductInfo productInfo = getProductInfoCommand.execute();
- Long cityId = productInfo.getCityId();
- GetCityNameCommand getCityNameCommand = new GetCityNameCommand(cityId); // 獲取本地內存(cityName)的代碼會被信號量進行資源隔離 String cityName = getCityNameCommand.execute();
- productInfo.setCityName(cityName);
- System.out.println(productInfo); return "success";}