最近起了個專案訊息中心,用來中轉各個系統中產生的訊息,用到的是RabbitMQ,由於UAT環境、生產環境每臺消費者服務都是多臺,有些訊息要求按順序消費,所以需要採取一定的措施保證訊息的順序消費,下面講下我們不斷優化的三種方法:
1、我們最開始考慮的比較簡單,採用的direct交換機,指定特定消費者伺服器監聽佇列,其他消費者伺服器不監聽。比如現在有C1、C2、C3三臺消費者機器,我們決定C1消費訊息,C2、C3不監聽。我們在啟動C1的時候,啟動指令碼中新增C1_IP,在程式碼中做處理,消費者伺服器啟動時,如果當前伺服器IP就是啟動指令碼的C1_IP,那就會由這臺C1來監聽並消費訊息。這種方式有個單點故障問題,如果C1伺服器當機,那麼整個訊息中心剩餘兩個節點都無法消費這個佇列,導致佇列訊息堆積。如果有豐富的監控措施,那麼監控到C1當機後,可通過手動配置C2_IP(或者C3_IP)到啟動指令碼,重啟C2伺服器(C3伺服器)消費訊息。
2、為了解決單點故障問題,我們採用了fanout交換機,每個消費者建立一個專用的queue,這樣如果生產者產生兩條有先後順序的訊息m1和m2(它們有公共的批次號batchNo和唯一的訊息編號msgID),就會給每個queue都推送,如下圖所示。同時消費者消費的時候需要配合資料庫共同實施,消費者監聽到訊息後就入庫(落庫內容包括m1訊息資訊和消費者IP),根據msgID唯一索引性如果入庫了則自己拋棄訊息,消費m2時,需要從庫表中取出m1的消費者IP是否是當前IP,如果不是則拋棄訊息。但是這個方案有個缺點:如果consumer1消費了m1後掛掉了,m2只能等到consumer1正常後才能消費,無法轉移到其他消費者進行消費,這樣會對一些業務場景不友好(當然這個地方可以考慮死信交換機死信佇列進行轉移,只不過架構更復雜了)。
3.第三種方式跟第二種類似,採用fanout交換機,每個消費者建立一個專用的queue。但是沒有藉助資料庫,而是通過訪問rabbitMQ的API介面,獲取這三個佇列的所有消費者的IP放到list中,消費者監聽到訊息後,判斷自己的ip是否是ip集合裡面的最小值,如果是則消費,如果否則拋棄訊息。一旦最小IP的消費者當機後,則list種就會只剩下兩個IP,後續的訊息選定的消費者就會從這兩個IP中選擇最小IP消費。同理它也有第二種方案的缺點。
最後附上通過rabbitmq的api獲取minIP的程式碼(入參consumerIps是初始size=0的list),如下:
private String findUsefulMinIP(List<String> consumerIps) { String minIp = null; SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setConnectTimeout(20000); try { RestTemplate rest = new RestTemplateBuilder().basicAuthentication(username, password).build(); rest.setRequestFactory(requestFactory); JSONArray result2 = rest.getForObject(moccMQApiUrl, JSONArray.class); if(result2 != null && result2.size() > 0) { log.info("===clear the ips===new query start==="); consumerIps.clear(); } for(int m=0; m<result2.size(); m++) { LinkedHashMap itmap = (LinkedHashMap) result2.get(m); LinkedHashMap queueMap = (LinkedHashMap)itmap.get("queue"); if(!queueMap.values().stream().anyMatch(v -> v.toString().indexOf(moccQueue)>=0)) { continue; } LinkedHashMap consumerMap = (LinkedHashMap)itmap.get("channel_details"); consumerIps.add((String)consumerMap.get("peer_host")); } log.info("===query from mq===consumerIps={}", consumerIps); } catch (RestClientException e) { log.error(e.getMessage(), e); } minIp = Collections.min(consumerIps); return minIp; }