一、如何保證消息隊列的高可用
1. RabbitMQ的高可用性
rabbitmq有三種模式:單機模式,普通集群模式,鏡像集群模式
- 普通集群模式:多臺機器部署,每個機器放一個rabbitmq實例,但是創建的queue只會放在一個rabbitmq實例上,每個實例同步queue的元數據。如果消費時連的是其他實例,那個實例會從queue所在實例拉取數據。這就會導致拉取數據的開銷,如果那個放queue的實例宕機了,那么其他實例就無法從那個實例拉取,即便開啟了消息持久化,讓rabbitmq落地存儲消息的話,消息不一定會丟,但得等這個實例恢復了,然后才可以繼續從這個queue拉取數據,這就沒什么高可用可言,主要是提供吞吐量,讓集群中多個節點來服務某個queue的讀寫操作。
- 鏡像集群模式:queue的元數據和消息都會存放在多個實例,每次寫消息就自動同步到多個queue實例里。這樣任何一個機器宕機,其他機器都可以頂上,但是性能開銷太大,消息同步導致網絡帶寬壓力和消耗很重,另外,沒有擴展性可言,如果queue負載很重,加機器,新增的機器也包含了這個queue的所有數據,并沒有辦法線性擴展你的queue。此時,需要開啟鏡像集群模式,在rabbit管理控制臺新增一個策略,將數據同步到指定數量的節點,然后你再次創建queue的時候,應用這個策略,就會自動將數據同步到其他的節點上去了
2. kafka的高可用性
kafka架構:多個broker組成,每個broker是一個節點;創建一個topic,這個topic可以劃分為多個partition,每個partition可以存在于不同的broker上,每個partition就放一部分數據。
它是一個分布式消息隊列,就是說一個topic的數據,是分散放在多個機器上的,每個機器就放一部分數據。
kafka 0.8以前,是沒有HA機制的,就是任何一個broker宕機了,那個broker上的partition就廢了,沒法寫也沒法讀,沒有什么高可用性可言。
kafka 0.8以后,提供了HA機制,就是replica副本機制。每個partition的數據都會同步到吉他機器上,形成自己的多個replica副本。然后所有replica會選舉一個leader出來,那么生產和消費都跟這個leader打交道,然后其他replica就是follower。寫的時候,leader會負責把數據同步到所有follower上去,讀的時候就直接讀leader上數據即可。kafka會均勻的將一個partition的所有replica分布在不同的機器上,從而提高容錯性。
如果某個broker宕機了也沒事,它上面的partition在其他機器上都有副本的,如果這上面有某個partition的leader,那么此時會重新選舉一個新的leader出來,大家繼續讀寫那個新的leader即可。這就有所謂的高可用性了。
寫數據的時候,生產者就寫leader,然后leader將數據落地寫本地磁盤,接著其他follower自己主動從leader來pull數據。一旦所有follower同步好數據了,就會發送ack給leader,leader收到所有follower的ack之后,就會返回寫成功的消息給生產者。
消費的時候,只會從leader去讀,但是只有當消息已經被所有follower都同步成功返回ack的時候,這個消息才會被消費者讀到。
二、如何保證消息不被重復消費
kafka重復消費的情況:
kafka有個offset的概念,就是每個消息寫進去,都有一個offset,代表他的序號,然后consumer消費了數據之后,每隔一段時間,會把自己消費過的消息的offset提交一下,下次重啟時,從上次消費到的offset來繼續消費。但是offset沒來得及提交就重啟,這部分會再次消費一次。
怎么保證消息隊列消費的冪等性:
- 比如數據寫庫,可以先根據主鍵查一下,如果這數據都有了,就update
- 比如寫redis,那沒問題,因為每次都是set,天然冪等性
- 如果不是上面兩個場景,那做的稍微復雜一點,需要讓生產者發送每條數據的時候,里面加一個全局唯一的id,類似訂單id之類的東西,然后消費到了后,先根據這個id去比如redis里查一下,之前消費過嗎?如果沒有消費過,就處理,然后這個id寫redis。如果消費過了,那就別處理了,保證別重復處理相同的消息即可。
- 還有比如基于數據庫的唯一鍵來保證重復數據不會重復插入多條,重復數據拿到了以后我們插入的時候,因為有唯一鍵約束了,所以重復數據只會插入報錯,不會導致數據庫中出現臟數據
三、如何保證消息的可靠性傳輸(如何處理消息丟失的問題)
丟數據,mq一般分為兩種,要么是mq自己弄丟了,要么是我們消費的時候弄丟了
1. rabbitmq
- 生產者弄丟了數據:
生產者將數據發送到rabbitmq的時候,因為網絡或者其他問題,半路給搞丟了。
此時可以選擇用rabbitmq提供的事務功能,就是生產者發送數據之前開啟rabbitmq事務(channel.txSelect),然后發送消息,如果消息沒有成功被rabbitmq接收到,那么生產者會收到異常報錯,此時就可以回滾事務(channel.txRollback),然后重試發送消息;如果收到了消息,那么可以提交事務(channel.txCommit)。但是rabbitmq事務機會降低吞吐量,因為太耗性能。
所以一般是開啟confirm模式,在生產者那里設置開啟confirm模式后,每次寫消息都會分配一個唯一的id,然后如果寫入了rabbitmq中,rabbitmq會回傳一個ack消息,告訴你說這個消息ok了。如果rabbitmq沒能處理這個消息,會回調你一個nack接口,告訴你這個消息接收失敗,你可以重試。而且你可以結合這個機制自己在內存里維護每個消息id的狀態,如果超過一定時間還沒接收到這個消息的回調,那么你可以重發
事務機制和cnofirm機制最大的不同在于,事務機制是同步的,你提交一個事務之后會阻塞在那兒,但是confirm機制是異步的,你發送個消息之后就可以發送下一個消息,然后那個消息rabbitmq接收了之后會異步回調你一個接口通知你這個消息接收到了。
- rabbitmq丟失數據:
開啟rabbitmq的持久化。設置持久化有兩個步驟,第一個是創建queue的時候將其設置為持久化的,這樣就可以保證rabbitmq持久化queue的元數據,但是不會持久化queue里的數據;第二個是發送消息的時候將消息的deliveryMode設置為2,就是將消息設置為持久化的,此時rabbitmq就會將消息持久化到磁盤上去。必須要同時設置這兩個持久化才行,rabbitmq哪怕是掛了,再次重啟,也會從磁盤上重啟恢復queue,恢復這個queue里的數據。
而且持久化可以跟生產者那邊的confirm機制配合起來,只有消息被持久化到磁盤之后,才會通知生產者ack了,所以哪怕是在持久化到磁盤之前,rabbitmq掛了,數據丟了,生產者收不到ack,你也是可以自己重發的。
- 消費端弄丟了數據
消費的時候,剛消費到,還沒處理,結果進程掛了,比如重啟,此時rabbitmq認為消費過了,這數據就丟了。
用rabbitmq提供的ack機制,簡單來說,就是關閉rabbitmq自動ack,可以通過一個api來調用,然后每次代碼里確保處理完的時候,再程序里ack一把。這樣的話,如果還沒處理完,就沒有ack,那rabbitmq就認為你還沒處理完,這個時候rabbitmq會把這個消費分配給別的consumer去處理,消息是不會丟的。
2. kafka
- 消費端弄丟了數據
唯一可能就是消費到了這個消息,然后消費者那邊自動提交了offset,讓kafka以為你已經消費好了這個消息,其實你剛準備處理這個消息,你還沒處理,你自己就掛了,此時這條消息就丟了。
只要關閉自動提交offset,在處理完之后自己手動提交offset,就可以保證數據不會丟。但是此時確實還是會重復消費,比如剛處理完,還沒提交offset,結果自己掛了,此時肯定會重復消費一次,自己保證冪等性就好了。
- kafka弄丟了數據
kafka某個broker宕機,然后重新選舉partiton的leader時。大家想想,要是此時其他的follower剛好還有些數據沒有同步,結果此時leader掛了,然后選舉某個follower成leader之后,中間這部分數據就丟失了。
所以此時一般是要求起碼設置如下4個參數:
(1)給這個topic設置replication.factor參數:這個值必須大于1,要求每個partition必須有至少2個副本
(2)在kafka服務端設置min.insync.replicas參數:這個值必須大于1,這個是要求一個leader至少感知到有至少一個follower還跟自己保持聯系,沒掉隊,這樣才能確保leader掛了還有一個follower
(3)在producer端設置acks=all:這個是要求每條數據,必須是寫入所有replica之后,才能認為是寫成功了
(4)在producer端設置retries=MAX(很大很大很大的一個值,無限次重試的意思):這個是要求一旦寫入失敗,就無限重試,卡在這里了
- 生產者會不會弄丟數據
如果按照上述的思路設置了ack=all,一定不會丟,要求是,你的leader接收到消息,所有的follower都同步到了消息之后,才認為本次寫成功了。如果沒滿足這個條件,生產者會自動不斷的重試,重試無限次。
三、如何保證消息的順序性
1. rabbitmq
拆分多個queue,每個queue一個consumer,就是多一些queue而已,確實是麻煩點;或者就一個queue但是對應一個consumer,然后這個consumer內部用內存隊列做排隊,然后分發給底層不同的worker來處理
2. kafka
寫入一個partition中的數據一定是有序的,生產者在寫的時候 ,可以指定一個key,比如指定訂單id作為key,這個訂單相關數據一定會被分發到一個partition中去。消費者從partition中取出數據的時候也一定是有序的,把每個數據放入對應的一個內存隊列,一個partition中有幾條相關數據就用幾個內存隊列,消費者開啟多個線程,每個線程處理一個內存隊列。