1. 程式人生 > >架構師系列文:通過Spring Cloud元件Hystrix合併請求 架構師入門:Spring Cloud系列,Hystrix與Eureka的整合

架構師系列文:通過Spring Cloud元件Hystrix合併請求 架構師入門:Spring Cloud系列,Hystrix與Eureka的整合

    在前文裡,我們講述了通過Hystrix進行容錯處理的方式,這裡我們將講述通過Hystrix合併請求的方式

    哪怕一個URL請求呼叫的功能再簡單,Web應用服務都至少會開啟一個執行緒來提供服務,換句話說,有效降低URL請求數能很大程度上降低系統的負載。通過Hystrix提供的“合併請求”機制,我們能有效地降低請求數量。

    在如下的HystrixMergeDemo.java裡,我們將收集2秒內到達的所有“查詢訂單”的請求,並把它們合併到一個物件中傳輸給後臺,後臺則是根據多個請求引數統一返回查詢結果,這種基於合併的做法將比每次只處理一個請求的方式要高效得多,程式碼比較長,我們按類來說明。    

1    //省略必要的package和import的程式碼
2    class OrderDetail{ //訂單業務類,其中包含2個屬性
3      private String orderId;
4      private String orderOwner;
5      //省略針對orderId和orderOwner的get和set方法
6          //重寫toString方法,方便輸出
7          public String toString() {        
8            return "orderId: " + orderId + ", orderOwner: " + orderOwner ;
9 } 10 } 11 //合併訂單請求的處理器 12 class OrderHystrixCollapser extends HystrixCollapser<Map<String, OrderDetail>, OrderDetail, String> 13 { 14 String orderId; 15 //在建構函式裡傳入請求引數 16 public OrderHystrixCollapser(String orderId) 17 { this.orderId = orderId;}
18 //指定根據orderId去請求OrderDetail 19 public String getRequestArgument() 20 { return orderId; } 21 //建立請求命令 22 protected HystrixCommand<Map<String, OrderDetail>> createCommand( 23 Collection<CollapsedRequest<OrderDetail, String>> requests) 24 { return new MergerCommand(requests); } 25 //把請求得到的結果和請求關聯到一起 26 protected void mapResponseToRequests(Map<String, OrderDetail> batchResponse, 27 Collection<CollapsedRequest<OrderDetail, String>> requests) { 28 for (CollapsedRequest<OrderDetail, String> request : requests) 29 { 30 // 請注意這裡是得到單個請求的結果 31 OrderDetail oneOrderDetail = batchResponse.get(request.getArgument()); 32 // 把結果關聯到請求中 33 request.setResponse(oneOrderDetail); 34 } 35 } 36 }

    在第2行,我們定義了OrderDetail類,這裡,我們將合併針對該類物件的請求。

    在第12行,我們定義了合併訂單的處理器OrderHystrixCollapser類, 它繼承(extends)了HystrixCollapser<Map<String, OrderDetail>, OrderDetail, String>類,而HystrixCollapser泛型中包含了3個引數,其中第一個引數Map<String, OrderDetail>表示該合併處理器合併請求後返回的結果型別,第二個引數表示是合併OrderDetail型別的物件,第三個引數則表示是根據String型別的請求引數來合併物件。

    在第19行裡,我們指定了是根據String型別的OrderId引數來請求OrderDetail物件,在第22行的createCommand方法裡,我們指定了是呼叫MergerCommand方法來請求多個OrderDetail,在第26行的mapResponseToRequests方法裡,我們是用第28行的for迴圈,依次把batchResponse物件中包含的多個的查詢結果設定到request物件裡,由於request是引數requests裡的元素,所以執行完第28行的for迴圈後,requests物件就能關聯到合併後的查詢結果。    

37    class MergerCommand extends HystrixCommand<Map<String, OrderDetail>> {
38        //用orderDB模擬資料庫中的資料
39        static HashMap<String,String> orderDB = new HashMap<String,String> ();
40        static     {
41            orderDB.put("1","Peter");
42            orderDB.put("2","Tom");
43            orderDB.put("3","Mike");
44        }    
45        Collection<CollapsedRequest<OrderDetail, String>> requests;
46        public MergerCommand(Collection<CollapsedRequest<OrderDetail, String>> requests) {
47            super(Setter.withGroupKey(HystrixCommandGroupKey.Factory
48                    .asKey("mergeDemo")));
49            this.requests = requests;
50        }
51        //在run方法里根據請求引數返回結果
52        protected Map<String, OrderDetail> run() throws Exception {
53            List<String> orderIds = new ArrayList<String>();
54            //通過for迴圈,整合引數
55            for(CollapsedRequest<OrderDetail, String> request : requests)    
56        { orderIds.add(request.getArgument());     }
57            // 呼叫服務,根據多個訂單Id獲得多個訂單物件
58            Map<String, OrderDetail> ordersHM = getOrdersFromDB(orderIds);
59            return ordersHM;
60        }    
61        //用HashMap模擬資料庫,從資料庫中獲得物件
62        private Map<String, OrderDetail> getOrdersFromDB(List<String> orderIds) {
63            Map<String, OrderDetail> result = new HashMap<String, OrderDetail>();
64            for(String orderId : orderIds) {
65                OrderDetail order = new OrderDetail();
66                //這個本該是從資料庫裡得到,但為了模擬,僅從HashMap裡取資料
67                order.setOrderId(orderId);
68                order.setOrderOwner(orderDB.get(orderId) );            
69                result.put(orderId, order);
70            }
71            return result;
72        }
73    }

    在MergerCommand類的第38到44行裡,我們用了orderDB物件來模擬資料庫裡儲存的訂單資料。在第46行的建構函式裡,我們用傳入的requests物件來構建本類裡的同名物件,在這個傳入的requests物件裡,已經包含了合併後的請求。

    在第52行的run方法裡,我們通過第55行的for迴圈,依次遍歷requests物件,並組裝包含請求引數集合的orderIds物件,隨後在第58行裡,通過getOrdersFromDB方法,根據List型別的orderIds引數,模擬地從資料庫裡讀取資料。        

74    public class HystrixMergeDemo{
75        public static void main(String[] args){
76            // 收集2秒內發生的請求,合併為一個命令執行
77            ConfigurationManager.getConfigInstance().setProperty(    "hystrix.collapser.default.timerDelayInMilliseconds", 2000);
78            // 初始化請求上下文
79            HystrixRequestContext context = HystrixRequestContext    .initializeContext();
80            // 建立3個請求合併處理器
81            OrderHystrixCollapser collapser1 = new OrderHystrixCollapser("1");
82            OrderHystrixCollapser collapser2 = new OrderHystrixCollapser("2");
83            OrderHystrixCollapser collapser3 = new OrderHystrixCollapser("3");        
84            // 非同步執行
85            Future<OrderDetail> future1 = collapser1.queue();
86            Future<OrderDetail> future2 = collapser2.queue();
87            Future<OrderDetail> future3 = collapser3.queue();        
88            try {
89                System.out.println(future1.get());
90                System.out.println(future2.get());
91                System.out.println(future3.get());
92            } catch (InterruptedException e) {
93                e.printStackTrace();
94            } catch (ExecutionException e) {
95                e.printStackTrace();
96            }
97            /關閉請求上下文
98            context.shutdown();
99        }
100    }

    第74行定義的HystrixMergeDemo類裡包含著main方法,在第77行裡,我們設定了合併請求的視窗時間是2秒,在第81到83行,建立了3個合併處理器物件,從第85到87行,我們是通過queue 方法,以非同步的方式啟動了三個處理器,並在第89到91行裡,輸出了三個處理器返回的結果。這個程式的執行結果如下。     

1    orderId: 1, orderOwner: Peter
2    orderId: 2, orderOwner: Tom
3    orderId: 3, orderOwner: Mike

     雖然在main方法裡,我們發起了3次呼叫,但由於這些呼叫是發生在2秒內的,所以會被合併處理,下面我們結合上述針對類和方法的說明,歸納下合併處理3個請求的流程。

    步驟一,在程式碼的81到83行裡,我們是通過OrderHystrixCollapser型別的collapser1等三個物件來傳入待合併處理的請求,OrderHystrixCollapser類會通過第16行的建構函式,分別接收三個物件傳入的orderId引數,並通過第22行的createCommand方法,呼叫MergerCommand類的方法執行“根據訂單Id查訂單”的業務。

    這裡說明下,由於在OrderHystrixCollapser內第16行的getRequestArgument方法裡,我們指定了查詢引數名是orderId,所以createCommand方法的requests引數,會用orderId來設定查詢請求,同時,MergerCommand類中的相關方法也會用該物件來查詢OrderDetail資訊。

    步驟二,由於在createCommand方法裡,呼叫了MergerCommand類的建構函式,所以會觸發該類第52行的run方法,在這個方法裡,通過第55行和第56行的for迴圈,把request請求中包含的多個Argument(也就是OrderId)放入到orderIds這個List型別的物件中,隨後通過第58行的getOrdersFromDB方法,根據這些orderIds去找對應的OrderDetail物件。

   步驟三,在getORdersFromDB方法裡,找到對應的多個OrderDetail物件,並組裝成Map<String, OrderDetail>型別的result物件返回,然後按呼叫鏈的關係,層層返回給OrderHystrixCollapser類。

    步驟四,在OrderHystrixCollapser類的mapResponseToRequests方法裡,通過for迴圈,把多次請求的結果組裝到requests物件中。由於requests物件是Collection<CollapsedRequest<OrderDetail, String>>型別的,其中用String型別的OrderId關聯到了一個OrderDetail物件,所以這裡會把合併查詢的結果再拆散給3次請求,具體而言,會把3個OrderDetail物件對應地返回給第85行到第87行通過queue呼叫的3個請求。

    這裡請注意,雖然通過合併請求的處理方法能降低URL請求的數量,但如果合併後的URL請求數過多,會撐爆掉合併處理器(這裡是OrderHystrixCollapser類)的快取。比如在某項 目裡,雖然只設置了合併5秒內的請求,但正好趕上秒殺活動,在這個視窗期內的請求數過萬,那麼就有可能出問題。

    所以一般會在上線前,先通過測試確定合併處理器的快取容量,隨後再預估下平均每秒的可能訪問數,然後再據此設定合併的視窗時間。

     本人之前寫的和本文有關的Spring Cloud其它相關文章。

 架構師入門:Spring Cloud系列,Hystrix與Eureka的整合

  Hystrix針對不可用服務的保護機制以及引入快取

 通過案例瞭解Hystrix的各種基本使用方式 

  Ribbon整合Eureka元件,以實現負載均衡      Spring Clould負載均衡重要元件:Ribbon中重要類的用法      架構師入門:搭建雙註冊中心的高可用Eureka架構(基於專案實戰)      架構師入門:搭建基本的Eureka架構(從專案裡抽取)     藉助Maven入手Spring Boot第一個程式