小菜最近用到RabbitMQ,由於之前瞭解過其他消息中間件,算是有些基礎,所以隨手從網上搜了幾篇文章,準備大概瞭解下RabbitMQ的消息模型,沒想到網上文章千篇一律,寫一大堆內容,就是說不明白到底怎麼回事,真是逼小菜寫博客… 首先說明本文只適合有消息中間件基礎的讀者,本文不會講解基礎概念,而是一針 ...
小菜最近用到RabbitMQ,由於之前瞭解過其他消息中間件,算是有些基礎,所以隨手從網上搜了幾篇文章,準備大概瞭解下RabbitMQ的消息模型,沒想到網上文章千篇一律,寫一大堆內容,就是說不明白到底怎麼回事,真是逼小菜寫博客…
首先說明本文只適合有消息中間件基礎的讀者,本文不會講解基礎概念,而是一針見血的指明RabbitMQ該怎麼用,告訴讀者RabbitMQ能做什麼,而不是像網路上其他文章那樣花里胡哨抓不住重點。
好了,直入正題。
simple簡單隊列
這種隊列,純屬RabbitMQ搞的一個花樣,僅僅是個概念而已!並不是實際的隊列類型!他就是在假設某個隊列只有一個消費者,也就是說,讀者在實際使用中,某個隊列傻傻的只用一個消費者去消費,這就叫simple簡單隊列啦,應用場景極少,一般情況下消費端都會有多個消費者。
Fair dispatch公平分發
這種隊列讓人一看,有點蒙逼,實際上這個概念非常非常簡單,如果讀者用過redis的話,這個隊列模式很像redis的list用法,只不過redis是拉取模型,而mq是推送模型。
這個公平,可不是說消息平均的發送給消費者,恰恰相反,消費者消費消息的多少,完全取決於消費者的處理能力,能者多勞,相當於消費者主動從mq中取消息,而不是被mq安排消息。
實現上也不難理解,消費端消費數據時,會有一個確認消費完成的動作,mq收到消費完成的通知後,才會繼續向該消費者發送消息,因此,如果消費者處理速度快,那麼最終mq向它發送的消息就多,如果消費者處理的慢,mq向它發送的消息就少。
在這小菜貼出java代碼實現的關鍵點:
當然,這是消費端代碼,僅僅在消費端做處理即可,對於生產端來說是透明的,不需要做任何處理。
Round-robin輪詢分發
所謂輪詢分發,就是公平分發的退化版,打開自動通知,去掉手動通知,去掉消費端消費條數限制,就是輪詢分發啦!!!
其實輪詢分發就是利用了自動通知參數,開啟了自動通知,mq根據一個簡單的規則(比如取模運算),先確定好哪些消息發送給哪些消費者,無論消費者處理能力如何,這些消息都得讓你處理,因此每個消費者最終處理的消息數量,是相同的(忽略"消息數量/消費者"不能整除的情況)。
這種模式很明顯是有問題的,首先,這種模式不能很好的利用消費端的性能差異,做不到真正意義上的負載均衡,浪費資源;其次(只是猜測),這種模型還有可能造成大量消息堆積在消費者容器中,這是非常危險的,不僅會造成消息丟失,還有可能壓垮消費者。
publish_subscribe發佈訂閱模式
RabbitMQ 中有一個交換機Exchanges的概念,發佈訂閱就是通過交換機實現的。
交換機的概念非常簡單,就是一個轉發器,有了交換機之後,生產端先把消息發送到交換機,然後交換機再把消息發送到與其綁定的消息隊列,這樣就解決了生成端如何把一條消息批量發送到多個消息隊列的問題。
交換機本身沒有數據存儲能力,僅僅是一個代理,可以理解成nginx。
因此,實現發佈訂閱的關鍵在於:
· 生產端(發佈端)直接發送消息到交換機,而不是具體的消息隊列。
· 多個消費端(訂閱端)將自己的消息隊列綁定到同一個交換機上。
這樣就實現了發佈訂閱。
routing路由模式
路由模式僅僅基於發佈訂閱搞了一點小事情,在發佈訂閱模式中,交換機無腦向所有與之綁定的消息隊列發送消息,而路由模式對交換機做了一些限制,它指定了一個route key,生產端向交換機發送消息時,指定消息的route key,消費端將消息隊列綁定到交換機時,也指定該隊列消費的route key,這樣一來,交換機就可以根據消息的route key,將該消息轉發到綁定(消費)該route key的消息隊列。
生產端關鍵點:
消費端關鍵點:
topic主題模式
RabbitMQ又開始搞花樣了,咋一看topic小菜還以為是kafka里的topic概念呢,弄的莫名其妙。
主題模式其實就是路由模式的一個加強,而且是非常非常非常簡單的一個加強:route key支持通配符。
主題模式和路由模式完全一樣,只不過是消費端route key不用寫死,增加了一個模糊匹配的功能,這樣在某些場景下,消費端就不用逐一綁定所有監聽的route key,直接用抽象的通配符表示即可,當然,這是針對消費端的優化,與生產端無關。
關於RabbitMQ的可靠性
消費端的消費可靠性,已經在"Fair dispatch公平分發"章節中做了介紹,即利用手動通知告訴mq消費成功,但通知也有不可達的可能,進而涉及到重發,具體的處理細節,讀者自行查閱資料。
生產端的提交可靠性,可以通過mq的回調機制實現,即生產端發送消息時自己維護一份已發送消息的集合,mq收到某條消息之後,會向生產端發送一個接收成功確認(體現在代碼中就是回調),然後生產端根據確認消息的唯一id,從自己維護的已發送消息集合中移除該消息,從而確保每條消息都成功發送到了mq。
假如某些消息未成功到達mq,那麼就不會有對應消息的確認,最終集合中會有剩餘元素(理想情況下是沒有的),這些剩餘元素,就是發送失敗的消息,需要重發。
簡單展示下代碼關鍵點( 生產端):
雖然確認機制可以保證消息的可靠性,但是必然帶來性能損失,因此到底需不需要開啟生產端或消費端的確認機制,需要根據業務場景具體分析。
一些註意事項
RabbitMQ的Connection是昂貴的,但Channel是廉價的,在多線程環境下,儘量創建少數Connection,然後在每個Connection中創建多個Channel,利用Channel實現Connection復用,從而提高系統性能。很像Java NIO里的Selector到Channel的多路復用。
生產端發送消息時,同一個Channel的basicPublish方法並不是線程安全的,因此更加體現出多Channel的重要性。如果生產端需要使用多線程發送消息,那麼必須創建多個Channel,每一個線程單獨使用一個Channel,但是這些Channel可以來自同一個Connection。假如線程數量過多,那麼也不可以無限制的創建Channel,需要使用Channel Pool(連接池)的思路去控制併發。
對於同一個Channel而言,發送消息和接受消息是互不影響的,可以進行併發操作。
吐槽
最後吐槽一下RabbitMQ客戶端API設計的真難用,同一個API竟然通過參數值重載,就比如向消息隊列發送消息是這樣:
channel.basicPublish("","隊列名稱", null, message.getBytes());
然後向交換機發消息是這樣:
channel.basicPublish("交換機名稱","route key", null, message.getBytes());
同一個方法,第二個參數的含義,竟然是通過第一個參數是否為空決定的,厲害了~