(新建的羣1039047324,歡迎對技術感興趣的朋友加入,羣內只聊技術,分享工作中容易踩的坑,以及如何避免踩坑,分享最新架構視頻)
消息隊列是分佈式應用間交換信息的重要組件,消息隊列可駐留在內存或磁盤上, 隊列可以存儲消息直到它們被應用程序讀走。
通過消息隊列,應用程序可以在不知道彼此位置的情況下獨立處理消息,或者在處理消息前不需要等待接收此消息。
所以消息隊列可以解決應用解耦、異步消息、流量削鋒等問題,是實現高性能、高可用、可伸縮和最終一致性架構中不可以或缺的一環。
現在比較常見的消息隊列產品主要有ActiveMQ、RabbitMQ、ZeroMQ、Kafka、RocketMQ等。
本文重點以RabbitMQ爲例。
1.爲什麼要使用消息隊列
六個字:解耦、異步、削峯
(1)解耦
傳統模式:
傳統模式的缺點:
- 系統間耦合性太強,如上圖所示,系統A在代碼中直接調用系統B和系統C的代碼,如果將來D系統接入,系統A還需要修改代碼,過於麻煩!
中間件模式:
中間件模式的優點:
- 將消息寫入消息隊列,需要消息的系統自己從消息隊列中訂閱,從而系統A不需要做任何修改。
(2)異步
傳統模式:
傳統模式的缺點:
- 一些非必要的業務邏輯以同步的方式運行,太耗費時間。
中間件模式:
中間件模式的優點:
- 將消息寫入消息隊列,非必要的業務邏輯以異步的方式運行,加快相應速度
(3)削峯
傳統模式:
傳統模式的缺點:
- 併發量大的時間,所有的請求直接懟到數據庫,造成數據庫連接異常
中間件模式:
中間件模式的優點:
- 系統A慢慢的按照數據庫能處理的併發量,從消息隊列中慢慢拉取消息。在生產中,這個短暫的高峯期積壓是允許的。
2.使用了消息隊列會有什麼缺點
主要在於系統的可用性、複雜性、一致性問題,引入消息隊列後,需要考慮MQ的可用性,萬一MQ崩潰了豈不是要爆炸?而且複雜性明顯提高了,需要考慮一些消息隊列的常見問題和解決方案,還有就是一致性問題,一條消息由多個消費者消費,萬一有一個消費者消費失敗了,就會導致數據不一致。
3.消息隊列如何選型
注: - 表示尚未查找到準確數據
綜合上面的材料得出以下兩點:
(1)中小型軟件公司,建議選RabbitMQ.一方面,erlang語言天生具備高併發的特性,而且他的管理界面用起來十分方便。
正所謂,成也蕭何,敗也蕭何!他的弊端也在這裏,雖然RabbitMQ是開源的,然而國內有幾個能定製化開發erlang的程序員呢?所幸,RabbitMQ的社區十分活躍,可以解決開發過程中遇到的bug,這點對於中小型公司來說十分重要。不考慮rocketmq和kafka的原因是,一方面中小型軟件公司不如互聯網公司,數據量沒那麼大,選消息中間件,應首選功能比較完備的,所以kafka排除。不考慮rocketmq的原因是,rocketmq是阿里出品,如果阿里放棄維護rocketmq,中小型公司一般抽不出人來進行rocketmq的定製化開發,因此不推薦。
(2)大型軟件公司,根據具體使用在rocketMq和kafka之間二選一。一方面,大型軟件公司,具備足夠的資金搭建分佈式環境,也具備足夠大的數據量。針對rocketMQ,大型軟件公司也可以抽出人手對rocketMQ進行定製化開發,畢竟國內有能力改JAVA源碼的人,還是相當多的。至於kafka,根據業務場景選擇,如果有日誌採集功能,肯定是首選kafka了。具體該選哪個,看使用場景。
4、如何保證消息隊列的高可用
(一)RabbitMQ
RabbitMQ有三種模式:單機模式,普通集羣模式,鏡像集羣模式
(1)單機模式
單機模式平常使用在開發或者本地測試場景,一般就是測試是不是能夠正確的處理消息,生產上基本沒人去用單機模式,風險很大。
(2)普通集羣模式
普通集羣模式就是啓動多個RabbitMQ實例。在你創建的queue,只會放在一個rabbtimq實例上,但是每個實例都同步queue的元數據。在消費的時候完了,上如果連接到了另外一個實例,那麼那個實例會從queue所在實例上拉取數據過來。
這種方式確實很麻煩,也不怎麼好,沒做到所謂的分佈式,就是個普通集羣。因爲這導致你要麼消費者每次隨機連接一個實例然後拉取數據,要麼固定連接那個queue所在實例消費數據,前者有數據拉取的開銷,後者導致單實例性能瓶頸。
而且如果那個放queue的實例宕機了,會導致接下來其他實例就無法從那個實例拉取,如果你開啓了消息持久化,讓RabbitMQ落地存儲消息的話,消息不一定會丟,得等這個實例恢復了,然後纔可以繼續從這個queue拉取數據。
這方案主要是提高吞吐量的,就是說讓集羣中多個節點來服務某個queue的讀寫操作。
(3)鏡像集羣模式
鏡像集羣模式是所謂的RabbitMQ的高可用模式,跟普通集羣模式不一樣的是,你創建的queue,無論元數據還是queue裏的消息都會存在於多個實例上,然後每次你寫消息到queue的時候,都會自動把消息到多個實例的queue裏進行消息同步。
優點在於你任何一個實例宕機了,沒事兒,別的實例都可以用。缺點在於性能開銷太大和擴展性很低,同步所有實例,這會導致網絡帶寬和壓力很重,而且擴展性很低,每增加一個實例都會去包含已有的queue的所有數據,並沒有辦法線性擴展queue。
開啓鏡像集羣模式可以去RabbitMQ的管理控制檯去增加一個策略,指定要求數據同步到所有節點的,也可以要求就同步到指定數量的節點,然後你再次創建queue的時候,應用這個策略,就會自動將數據同步到其他的節點上去了。
(二)Kafka
Kafka天生就是一個分佈式的消息隊列,它可以由多個broker組成,每個broker是一個節點;你創建一個topic,這個topic可以劃分爲多個partition,每個partition可以存在於不同的broker上,每個partition就放一部分數據。
kafka 0.8以前,是沒有HA機制的,就是任何一個broker宕機了,那個broker上的partition就廢了,沒法寫也沒法讀,沒有什麼高可用性可言。
kafka 0.8以後,提供了HA機制,就是replica副本機制。kafka會均勻的將一個partition的所有replica分佈在不同的機器上,來提高容錯性。每個partition的數據都會同步到吉他機器上,形成自己的多個replica副本。然後所有replica會選舉一個leader出來,那麼生產和消費都去leader,其他replica就是follower,leader會同步數據給follower。當leader掛了會自動去找replica,然後會再選舉一個leader出來,這樣就具有高可用性了。
寫數據的時候,生產者就寫leader,然後leader將數據落地寫本地磁盤,接着其他follower自己主動從leader來pull數據。一旦所有follower同步好數據了,就會發送ack給leader,leader收到所有follower的ack之後,就會返回寫成功的消息給生產者。(當然,這只是其中一種模式,還可以適當調整這個行爲)
消費的時候,只會從leader去讀,但是隻有一個消息已經被所有follower都同步成功返回ack的時候,這個消息纔會被消費者讀到。
5、如何保證消息不被重複消費?或者說,如何保證消息消費的冪等性
其實消息重複消費的主要原因在於回饋機制(RabbitMQ是ack,Kafka是offset),在某些場景中我們採用的回饋機制不同,原因也不同,例如消費者消費完消息後回覆ack, 但是剛消費完還沒來得及提交系統就重啓了,這時候上來就pull消息的時候由於沒有提交ack或者offset,消費的還是上條消息。
那麼如何怎麼來保證消息消費的冪等性呢?實際上我們只要保證多條相同的數據過來的時候只處理一條或者說多條處理和處理一條造成的結果相同即可,但是具體怎麼做要根據業務需求來定,例如入庫消息,先查一下消息是否已經入庫啊或者說搞個唯一約束啊什麼的,還有一些是天生保證冪等性就根本不用去管,例如redis就是天然冪等性。
還有一個問題,消費者消費消息的時候在某些場景下要放過消費不了的消息,遇到消費不了的消息通過日誌記錄一下或者搞個什麼措施以後再來處理,但是一定要放過消息,因爲在某些場景下例如spring-rabbitmq的默認回饋策略是出現異常就沒有提交ack,導致了一直在重發那條消費異常的消息,而且一直還消費不了,這就尷尬了,後果你會懂的。
6、如何保證消息的可靠性傳輸?或者說,如何處理消息丟失的問題
用 MQ 有個基本原則,就是數據不能多一條,也不能少一條,不能多,就是前面說的重複消費和冪等性問題。不能少,就是說這數據別搞丟了。那這個問題你必須得考慮一下。數據的丟失問題,可能出現在生產者、MQ、消費者中。
1) 生產者弄丟了數據
生產者將數據發送到 RabbitMQ 的時候,可能數據就在半路給搞丟了,因爲網絡問題啥的,都有可能。
此時可以選擇用 RabbitMQ 提供的事務功能,就是生產者發送數據之前開啓 RabbitMQ 事務channel.txSelect
,然後發送消息,如果消息沒有成功被 RabbitMQ 接收到,那麼生產者會收到異常報錯,此時就可以回滾事務channel.txRollback
,然後重試發送消息;如果收到了消息,那麼可以提交事務channel.txCommit
。
但是問題是,RabbitMQ 事務機制(同步)一搞,基本上吞吐量會下來,因爲太耗性能。
2)RabbitMQ 弄丟了數據
就是 RabbitMQ 自己弄丟了數據,這個你必須開啓 RabbitMQ 的持久化,就是消息寫入之後會持久化到磁盤,哪怕是 RabbitMQ 自己掛了,恢復之後會自動讀取之前存儲的數據,一般數據不會丟。除非極其罕見的是,RabbitMQ 還沒持久化,自己就掛了,可能導致少量數據丟失,但是這個概率較小。
設置持久化有兩個步驟:
- 創建 queue 的時候將其設置爲持久化
這樣就可以保證 RabbitMQ 持久化 queue 的元數據,但是不會持久化 queue 裏的數據。 - 第二個是發送消息的時候將消息的
deliveryMode
設置爲 2
就是將消息設置爲持久化的,此時 RabbitMQ 就會將消息持久化到磁盤上去。
必須要同時設置這兩個持久化纔行,RabbitMQ 哪怕是掛了,再次重啓,也會從磁盤上重啓恢復 queue,恢復這個 queue 裏的數據。
持久化可以跟生產者那邊的confirm
機制配合起來,只有消息被持久化到磁盤之後,纔會通知生產者ack
了,所以哪怕是在持久化到磁盤之前,RabbitMQ 掛了,數據丟了,生產者收不到ack
,你也是可以自己重發的。
注意,哪怕是你給 RabbitMQ 開啓了持久化機制,也有一種可能,就是這個消息寫到了 RabbitMQ 中,但是還沒來得及持久化到磁盤上,結果不巧,此時 RabbitMQ 掛了,就會導致內存裏的一點點數據丟失。
所以一般來說,如果你要確保說寫 RabbitMQ 的消息別丟,可以開啓confirm
模式,在生產者那裏設置開啓confirm
模式之後,你每次寫的消息都會分配一個唯一的 id,然後如果寫入了 RabbitMQ 中,RabbitMQ 會給你回傳一個ack
消息,告訴你說這個消息 ok 了。如果 RabbitMQ 沒能處理這個消息,會回調你一個nack
接口,告訴你這個消息接收失敗,你可以重試。而且你可以結合這個機制自己在內存裏維護每個消息 id 的狀態,如果超過一定時間還沒接收到這個消息的回調,那麼你可以重發。
事務機制和cnofirm
機制最大的不同在於,事務機制是同步的,你提交一個事務之後會阻塞在那兒,但是confirm
機制是異步的,你發送個消息之後就可以發送下一個消息,然後那個消息RabbitMQ 接收了之後會異步回調你一個接口通知你這個消息接收到了。
所以一般在生產者這塊避免數據丟失,都是用confirm
機制的。
3)消費端弄丟了數據
RabbitMQ 如果丟失了數據,主要是因爲你消費的時候,剛消費到,還沒處理,結果進程掛了,比如重啓了,那麼就尷尬了,RabbitMQ 認爲你都消費了,這數據就丟了。
這個時候得用 RabbitMQ 提供的ack
機制,簡單來說,就是你關閉 RabbitMQ 的自動ack
,可以通過一個 api 來調用就行,然後每次你自己代碼裏確保處理完的時候,再在程序裏ack
一把。這樣的話,如果你還沒處理完,不就沒有ack
?那 RabbitMQ 就認爲你還沒處理完,這個時候 RabbitMQ 會把這個消費分配給別的 consumer 去處理,消息是不會丟的。
7.如何保證消息的順序性
因爲在某些情況下我們扔進MQ中的消息是要嚴格保證順序的,尤其涉及到訂單什麼的業務需求,消費的時候也是要嚴格保證順序,不然會出大問題的。
先看看順序會錯亂的兩個場景:
rabbitmq:一個queue,多個consumer,這不明顯亂了
kafka:一個topic,一個partition,一個consumer,內部多線程,這不也明顯亂了
1)rabbitmq:拆分成多個queue,每個queue一個consumer,就是多一些queue而已,確實是麻煩點;或者就一個queue但是對應一個consumer,然後這個consumer內部用內存隊列做排隊,然後分發給底層不同的worker來處理
8、如何解決消息隊列的延時以及過期失效問題?消息隊列滿了以後該怎麼處理?有幾百萬消息持續積壓幾小時怎麼解決?
消息積壓:如果你積壓了幾百萬到上千萬的數據,即使消費者恢復了,也需要大概1小時的時間才能恢復過來.
一般這個時候,只能操作臨時緊急擴容了,具體操作步驟和思路如下:
先修復consumer的問題,確保其恢復消費速度,然後將現有cnosumer都停掉。
新建一個topic,partition是原來的10倍,臨時建立好原先10倍或者20倍的queue數量。
然後寫一個臨時的分發數據的consumer程序,這個程序部署上去消費積壓的數據,消費之後不做耗時的處理,直接均勻輪詢寫入臨時建立好的10倍數量的queue。
接着臨時徵用10倍的機器來部署consumer,每一批consumer消費一個臨時queue的數據。
這種做法相當於是臨時將queue資源和consumer資源擴大10倍,以正常的10倍速度來消費數據。
等快速消費完積壓數據之後,得恢復原先部署架構,重新用原先的consumer機器來消費消息。
消息丟失:假設你用的是rabbitmq,rabbitmq是可以設置過期時間的,就是TTL,如果消息在queue中積壓超過一定的時間就會被rabbitmq給清理掉,這個數據就沒了。那這就是第二個坑了。這就不是說數據會大量積壓在mq裏,而是大量的數據會直接搞丟。
這個情況下,就不是說要增加consumer消費積壓的消息,因爲實際上沒啥積壓,而是丟了大量的消息。我們可以採取一個方案,就是批量重導,這個我們之前線上也有類似的場景幹過。就是大量積壓的時候,我們當時就直接丟棄數據了,然後等過了高峯期以後,比如大家一起喝咖啡熬夜到晚上12點以後,用戶都睡覺了。
這個時候我們就開始寫程序,將丟失的那批數據,寫個臨時程序,一點一點的查出來,然後重新灌入mq裏面去,把白天丟的數據給他補回來。也只能是這樣了。
假設1萬個訂單積壓在mq裏面,沒有處理,其中1000個訂單都丟了,你只能手動寫程序把那1000個訂單給查出來,手動發到mq裏去再補一次
9、RabbitMQ 有哪些重要的組件
ConnectionFactory(連接管理器):應用程序與Rabbit之間建立連接的管理器,程序代碼中使用;
Channel(信道):消息推送使用的通道;
Exchange(交換器):用於接受、分配消息;
Queue(隊列):用於存儲生產者的消息;
RoutingKey(路由鍵):用於把生成者的數據分配到交換器上;
BindingKey(綁定鍵):用於把交換器的消息綁定到隊列上;
10、RabbitMQ 有幾種廣播類型?(交換器類型)
fanout: 所有bind到此exchange的queue都可以接收消息(純廣播,綁定到RabbitMQ的接受者都能收到消息);
direct: 通過routingKey和exchange決定的那個唯一的queue可以接收消息;
topic: 是direct exchange的通配符模式,所有符合routingKey(可以是一個表達式)的routingKey所bind的queue可以接收消息;
headers:很少使用,性能差,消息頭交換機使用消息頭的屬性進行消息路由。
11、Kafka 可以脫離 zookeeper 單獨使用嗎?爲什麼?
kafka 不能脫離 zookeeper 單獨使用,因爲 kafka 使用 zookeeper 管理和協調 kafka 的節點服務器。