RabbitMQ、Kafka、RocketMQ和ActiveMQ,肝了我一個月,原理是什么,如何選型,本文會告訴你答案。
消息隊列中間件重要嗎?面試必問問題之一,你說重不重要。我有時會問同事,為啥你用RabbitMQ,不用Kafka,或者RocketMQ呢,他給我的回答“因為公司用的就是這個,大家都這么用”,如果你去面試,直接就被Pass,今天這篇文章,告訴你如何回答。
這篇文章純理論,主要整理網(wǎng)絡(luò)資料,肝了我整整一個月!文章依然延續(xù)上幾篇的風(fēng)格,很長,長到我只整理排版,手都整麻了。全文2.5萬字,建議先收藏,后續(xù)面試、或者技術(shù)選型,再拿出來喵喵,不BB,上思維導(dǎo)圖!
消息隊列目前主要2種模式,分別為“點(diǎn)對點(diǎn)模式”和“發(fā)布/訂閱模式”。
一個具體的消息只能由一個消費(fèi)者消費(fèi)。多個生產(chǎn)者可以向同一個消息隊列發(fā)送消息;但是,一個消息在被一個消息者處理的時候,這個消息在隊列上會被鎖住或者被移除并且其他消費(fèi)者無法處理該消息。需要額外注意的是,如果消費(fèi)者處理一個消息失敗了,消息系統(tǒng)一般會把這個消息放回隊列,這樣其他消費(fèi)者可以繼續(xù)處理。
單個消息可以被多個訂閱者并發(fā)的獲取和處理。一般來說,訂閱有兩種類型:
對消息隊列進(jìn)行技術(shù)選型時,需要通過以下指標(biāo)衡量你所選擇的消息隊列,是否可以滿足你的需求:
下圖是從網(wǎng)上摘抄過來的,可以看到主流MQ的對比:
下面簡單介紹常用的消息隊列:
優(yōu)點(diǎn):
缺點(diǎn):
總結(jié):
優(yōu)點(diǎn):
缺點(diǎn):
總結(jié):
優(yōu)點(diǎn):
缺點(diǎn):
總結(jié):
優(yōu)點(diǎn)
缺點(diǎn):
Kafka 是由 Linkedin 公司開發(fā)的,它是一個分布式的,支持多分區(qū)、多副本,基于 Zookeeper 的分布式消息流平臺,它同時也是一款開源的基于發(fā)布訂閱模式的消息引擎系統(tǒng)。
一個典型的 Kafka 集群中包含若干Producer(可以是web前端產(chǎn)生的Page View,或者是服務(wù)器日志,系統(tǒng)CPU、Memory等),若干broker(Kafka支持水平擴(kuò)展,一般broker數(shù)量越多,集群吞吐率越高),若干Consumer Group,以及一個Zookeeper集群。Kafka通過Zookeeper管理集群配置,選舉leader,以及在Consumer Group發(fā)生變化時進(jìn)行rebalance。Producer使用push模式將消息發(fā)布到broker,Consumer使用pull模式從broker訂閱并消費(fèi)消息。
在 Kafka 中,我們把產(chǎn)生消息的那一方稱為生產(chǎn)者,比如我們經(jīng)?;厝ヌ詫氋徫铮愦蜷_淘寶的那一刻,你的登陸信息,登陸次數(shù)都會作為消息傳輸?shù)?Kafka 后臺,當(dāng)你瀏覽購物的時候,你的瀏覽信息,你的搜索指數(shù),你的購物愛好都會作為一個個消息傳遞給 Kafka 后臺,然后淘寶會根據(jù)你的愛好做智能推薦,致使你的錢包從來都禁不住誘惑,那么這些生產(chǎn)者產(chǎn)生的消息是怎么傳到 Kafka 應(yīng)用程序的呢?發(fā)送過程是怎么樣的呢?
盡管消息的產(chǎn)生非常簡單,但是消息的發(fā)送過程還是比較復(fù)雜的,如圖:
我們從創(chuàng)建一個ProducerRecord 對象開始,ProducerRecord 是 Kafka 中的一個核心類,它代表了一組 Kafka 需要發(fā)送的 key/value 鍵值對,它由記錄要發(fā)送到的主題名稱(Topic Name),可選的分區(qū)號(Partition Number)以及可選的鍵值對構(gòu)成。
在發(fā)送 ProducerRecord 時,我們需要將鍵值對對象由序列化器轉(zhuǎn)換為字節(jié)數(shù)組,這樣它們才能夠在網(wǎng)絡(luò)上傳輸。然后消息到達(dá)了分區(qū)器。如果發(fā)送過程中指定了有效的分區(qū)號,那么在發(fā)送記錄時將使用該分區(qū)。如果發(fā)送過程中未指定分區(qū),則將使用key 的 hash 函數(shù)映射指定一個分區(qū)。如果發(fā)送的過程中既沒有分區(qū)號也沒有,則將以循環(huán)的方式分配一個分區(qū)。選好分區(qū)后,生產(chǎn)者就知道向哪個主題和分區(qū)發(fā)送數(shù)據(jù)了。ProducerRecord 還有關(guān)聯(lián)的時間戳,如果用戶沒有提供時間戳,那么生產(chǎn)者將會在記錄中使用當(dāng)前的時間作為時間戳。Kafka 最終使用的時間戳取決于 topic 主題配置的時間戳類型。然后,這條消息被存放在一個記錄批次里,這個批次里的所有消息會被發(fā)送到相同的主題和分區(qū)上。由一個獨(dú)立的線程負(fù)責(zé)把它們發(fā)到 Kafka Broker 上。
Kafka Broker 在收到消息時會返回一個響應(yīng),如果寫入成功,會返回一個 RecordMetaData 對象,它包含了主題和分區(qū)信息,以及記錄在分區(qū)里的偏移量,上面兩種的時間戳類型也會返回給用戶。如果寫入失敗,會返回一個錯誤。生產(chǎn)者在收到錯誤之后會嘗試重新發(fā)送消息,幾次之后如果還是失敗的話,就返回錯誤消息。
上面寫的有點(diǎn)多,總結(jié)一下流程:創(chuàng)建對象(主題、分區(qū)、key/value)-> 序列化數(shù)據(jù) -> 到達(dá)分區(qū)(可自己指定,也可以通過key hash)-> 放入批次(相同主題和分區(qū)) -> 獨(dú)立線程發(fā)送 -> 返回主題/分區(qū)/分區(qū)偏移量/時間戳。
Kafka 對于數(shù)據(jù)的讀寫是以分區(qū)為粒度的,分區(qū)可以分布在多個主機(jī)(Broker)中,這樣每個節(jié)點(diǎn)能夠?qū)崿F(xiàn)獨(dú)立的數(shù)據(jù)寫入和讀取,并且能夠通過增加新的節(jié)點(diǎn)來增加 Kafka 集群的吞吐量,通過分區(qū)部署在多個 Broker 來實現(xiàn)負(fù)載均衡的效果,下面我們看看數(shù)據(jù)如何選擇分區(qū)。
順序分配,消息是均勻的分配給每個 partition,即每個分區(qū)存儲一次消息,見下圖。輪訓(xùn)策略是 Kafka Producer 提供的默認(rèn)策略,如果你不使用指定的輪訓(xùn)策略的話,Kafka 默認(rèn)會使用順序輪訓(xùn)策略的方式。
本質(zhì)上看隨機(jī)策略也是力求將數(shù)據(jù)均勻地打散到各個分區(qū),但從實際表現(xiàn)來看,它要遜于輪詢策略,所以如果追求數(shù)據(jù)的均勻分布,還是使用輪詢策略比較好。事實上,隨機(jī)策略是老版本生產(chǎn)者使用的分區(qū)策略,在新版本中已經(jīng)改為輪詢了。
這個策略也叫做 key-ordering 策略,Kafka 中每條消息都會有自己的key,一旦消息被定義了 Key,那么你就可以保證同一個 Key 的所有消息都進(jìn)入到相同的分區(qū)里面,由于每個分區(qū)下的消息處理都是有順序的,故這個策略被稱為按消息鍵保序策略,如下圖所示
應(yīng)用程序使用 KafkaConsumer 從 Kafka 中訂閱主題并接收來自這些主題的消息,然后再把他們保存起來。應(yīng)用程序首先需要創(chuàng)建一個 KafkaConsumer 對象,訂閱主題并開始接受消息,驗證消息并保存結(jié)果。一段時間后,生產(chǎn)者往主題寫入的速度超過了應(yīng)用程序驗證數(shù)據(jù)的速度,這時候該如何處理?如果只使用單個消費(fèi)者的話,應(yīng)用程序會跟不上消息生成的速度,就像多個生產(chǎn)者像相同的主題寫入消息一樣,這時候就需要多個消費(fèi)者共同參與消費(fèi)主題中的消息,對消息進(jìn)行分流處理。Kafka 消費(fèi)者從屬于消費(fèi)者群組。一個群組中的消費(fèi)者訂閱的都是相同的主題,每個消費(fèi)者接收主題一部分分區(qū)的消息。下面是一個 Kafka 分區(qū)消費(fèi)示意圖。
上圖中的主題 T1 有四個分區(qū),分別是分區(qū)0、分區(qū)1、分區(qū)2、分區(qū)3,我們創(chuàng)建一個消費(fèi)者群組1,消費(fèi)者群組中只有一個消費(fèi)者,它訂閱主題T1,接收到 T1 中的全部消息。由于一個消費(fèi)者處理四個生產(chǎn)者發(fā)送到分區(qū)的消息,壓力有些大,需要幫手來幫忙分擔(dān)任務(wù),于是就演變?yōu)橄聢D
這樣一來,消費(fèi)者的消費(fèi)能力就大大提高了,但是在某些環(huán)境下比如用戶產(chǎn)生消息特別多的時候,生產(chǎn)者產(chǎn)生的消息仍舊讓消費(fèi)者吃不消,那就繼續(xù)增加消費(fèi)者。
如上圖所示,每個分區(qū)所產(chǎn)生的消息能夠被每個消費(fèi)者群組中的消費(fèi)者消費(fèi),如果向消費(fèi)者群組中增加更多的消費(fèi)者,那么多余的消費(fèi)者將會閑置,如下圖所示。
向群組中增加消費(fèi)者是橫向伸縮消費(fèi)能力的主要方式。總而言之,我們可以通過增加消費(fèi)組的消費(fèi)者來進(jìn)行水平擴(kuò)展提升消費(fèi)能力。這也是為什么建議創(chuàng)建主題時使用比較多的分區(qū)數(shù),這樣可以在消費(fèi)負(fù)載高的情況下增加消費(fèi)者來提升性能。另外,消費(fèi)者的數(shù)量不應(yīng)該比分區(qū)數(shù)多,因為多出來的消費(fèi)者是空閑的,沒有任何幫助。
Kafka 一個很重要的特性就是,只需寫入一次消息,可以支持任意多的應(yīng)用讀取這個消息。換句話說,每個應(yīng)用都可以讀到全量的消息。為了使得每個應(yīng)用都能讀到全量消息,應(yīng)用需要有不同的消費(fèi)組。對于上面的例子,假如我們新增了一個新的消費(fèi)組 G2,而這個消費(fèi)組有兩個消費(fèi)者,那么就演變?yōu)橄聢D這樣。在這個場景中,消費(fèi)組 G1 和消費(fèi)組 G2 都能收到 T1 主題的全量消息,在邏輯意義上來說它們屬于不同的應(yīng)用。
總結(jié)起來就是如果應(yīng)用需要讀取全量消息,那么請為該應(yīng)用設(shè)置一個消費(fèi)組;如果該應(yīng)用消費(fèi)能力不足,那么可以考慮在這個消費(fèi)組里增加消費(fèi)者。
我們從上面的消費(fèi)者演變圖中可以知道這么一個過程:最初是一個消費(fèi)者訂閱一個主題并消費(fèi)其全部分區(qū)的消息,后來有一個消費(fèi)者加入群組,隨后又有更多的消費(fèi)者加入群組,而新加入的消費(fèi)者實例分?jǐn)偭俗畛跸M(fèi)者的部分消息,這種把分區(qū)的所有權(quán)通過一個消費(fèi)者轉(zhuǎn)到其他消費(fèi)者的行為稱為重平衡,英文名也叫做 Rebalance 。如下圖所示。
重平衡非常重要,它為消費(fèi)者群組帶來了高可用性 和 伸縮性,我們可以放心的添加消費(fèi)者或移除消費(fèi)者,不過在正常情況下我們并不希望發(fā)生這樣的行為。在重平衡期間,消費(fèi)者無法讀取消息,造成整個消費(fèi)者組在重平衡的期間都不可用。另外,當(dāng)分區(qū)被重新分配給另一個消費(fèi)者時,消息當(dāng)前的讀取狀態(tài)會丟失,它有可能還需要去刷新緩存,在它重新恢復(fù)狀態(tài)之前會拖慢應(yīng)用程序。
消費(fèi)者通過向組織協(xié)調(diào)者(Kafka Broker)發(fā)送心跳來維護(hù)自己是消費(fèi)者組的一員并確認(rèn)其擁有的分區(qū)。對于不同不的消費(fèi)群體來說,其組織協(xié)調(diào)者可以是不同的。只要消費(fèi)者定期發(fā)送心跳,就會認(rèn)為消費(fèi)者是存活的并處理其分區(qū)中的消息。當(dāng)消費(fèi)者檢索記錄或者提交它所消費(fèi)的記錄時就會發(fā)送心跳。如果過了一段時間 Kafka 停止發(fā)送心跳了,會話(Session)就會過期,組織協(xié)調(diào)者就會認(rèn)為這個 Consumer 已經(jīng)死亡,就會觸發(fā)一次重平衡。如果消費(fèi)者宕機(jī)并且停止發(fā)送消息,組織協(xié)調(diào)者會等待幾秒鐘,確認(rèn)它死亡了才會觸發(fā)重平衡。在這段時間里,死亡的消費(fèi)者將不處理任何消息。在清理消費(fèi)者時,消費(fèi)者將通知協(xié)調(diào)者它要離開群組,組織協(xié)調(diào)者會觸發(fā)一次重平衡,盡量降低處理停頓。
重平衡是一把雙刃劍,它為消費(fèi)者群組帶來高可用性和伸縮性的同時,還有有一些明顯的缺點(diǎn)(bug),而這些 bug 到現(xiàn)在社區(qū)還無法修改。重平衡的過程對消費(fèi)者組有極大的影響。因為每次重平衡過程中都會導(dǎo)致萬物靜止,參考 JVM 中的垃圾回收機(jī)制,也就是 Stop The World ,STW。也就是說,在重平衡期間,消費(fèi)者組中的消費(fèi)者實例都會停止消費(fèi),等待重平衡的完成。而且重平衡這個過程很慢...
這里才是內(nèi)容的重點(diǎn),不僅需要知道Kafka的特性,還需要知道支持這些特性的原因:
RocketMQ是一個純Java、分布式、隊列模型的開源消息中間件,前身是MetaQ,是阿里參考Kafka特點(diǎn)研發(fā)的一個隊列模型的消息中間件,后開源給apache基金會成為了apache的頂級開源項目,具有高性能、高可靠、高實時、分布式特點(diǎn)。
先對常用的詞匯有個基本認(rèn)識,相關(guān)詞匯后面會再詳細(xì)介紹:
RockerMQ 中的消息模型就是按照主題模型所實現(xiàn)的,在主題模型中,消息的生產(chǎn)者稱為發(fā)布者(Publisher),消息的消費(fèi)者稱為訂閱者(Subscriber),存放消息的容器稱為主題(Topic)。RocketMQ 中的主題模型到底是如何實現(xiàn)的呢?
我們可以看到在整個圖中有 Producer Group、Topic、Consumer Group 三個角色,你可以看到圖中生產(chǎn)者組中的生產(chǎn)者會向主題發(fā)送消息,而主題中存在多個隊列,生產(chǎn)者每次生產(chǎn)消息之后是指定主題中的某個隊列發(fā)送消息的。
每個主題中都有多個隊列(這里還不涉及到 Broker),集群消費(fèi)模式下,一個消費(fèi)者集群多臺機(jī)器共同消費(fèi)一個 topic 的多個隊列,一個隊列只會被一個消費(fèi)者消費(fèi)。如果某個消費(fèi)者掛掉,分組內(nèi)其它消費(fèi)者會接替掛掉的消費(fèi)者繼續(xù)消費(fèi)。就像上圖中 Consumer1 和 Consumer2 分別對應(yīng)著兩個隊列,而 Consuer3 是沒有隊列對應(yīng)的,所以一般來講要控制消費(fèi)者組中的消費(fèi)者個數(shù)和主題中隊列個數(shù)相同。這個簡直和kafak一毛一樣啊!
當(dāng)然也可以消費(fèi)者個數(shù)小于隊列個數(shù),只不過不太建議。如下圖:
每個消費(fèi)組在每個隊列上維護(hù)一個消費(fèi)位置,為什么呢?因為我們剛剛畫的僅僅是一個消費(fèi)者組,我們知道在發(fā)布訂閱模式中一般會涉及到多個消費(fèi)者組,而每個消費(fèi)者組在每個隊列中的消費(fèi)位置都是不同的。如果此時有多個消費(fèi)者組,那么消息被一個消費(fèi)者組消費(fèi)完之后是不會刪除的(因為其它消費(fèi)者組也需要呀),它僅僅是為每個消費(fèi)者組維護(hù)一個消費(fèi)位移(offset),每次消費(fèi)者組消費(fèi)完會返回一個成功的響應(yīng),然后隊列再把維護(hù)的消費(fèi)位移加一,這樣就不會出現(xiàn)剛剛消費(fèi)過的消息再一次被消費(fèi)了。
可能你還有一個問題,為什么一個主題中需要維護(hù)多個隊列?答案是提高并發(fā)能力。的確,每個主題中只存在一個隊列也是可行的。你想一下,如果每個主題中只存在一個隊列,這個隊列中也維護(hù)著每個消費(fèi)者組的消費(fèi)位置,這樣也可以做到發(fā)布訂閱模式。如下圖:
所以總結(jié)來說,RocketMQ 通過使用在一個 Topic 中配置多個隊列,并且每個隊列維護(hù)每個消費(fèi)者組的消費(fèi)位置,實現(xiàn)了主題模式/發(fā)布訂閱模式。
講完了消息模型,我們理解起 RocketMQ 的技術(shù)架構(gòu)起來就容易多了。RocketMQ 技術(shù)架構(gòu)中有四大角色 NameServer、Broker、Producer、Consumer。這4大角色,已經(jīng)在基本概念中簡單解釋過,對于相關(guān)詞匯,這里再重點(diǎn)解釋一下。
聽完了上面的解釋你可能會覺得,這玩意好簡單。不就是這樣的么?
嗯?你可能會發(fā)現(xiàn)一個問題,這老家伙 NameServer 干啥用的,這不多余嗎?直接 Producer、Consumer 和 Broker 直接進(jìn)行生產(chǎn)消息,消費(fèi)消息不就好了么?但是,我們上文提到過 Broker 是需要保證高可用的,如果整個系統(tǒng)僅僅靠著一個 Broker 來維持的話,那么這個 Broker 的壓力會不會很大?所以我們需要使用多個 Broker 來保證負(fù)載均衡。如果說,我們的消費(fèi)者和生產(chǎn)者直接和多個 Broker 相連,那么當(dāng) Broker 修改的時候必定會牽連著每個生產(chǎn)者和消費(fèi)者,這樣就會產(chǎn)生耦合問題,而 NameServer 注冊中心就是用來解決這個問題的。
當(dāng)然,RocketMQ 中的技術(shù)架構(gòu)肯定不止前面那么簡單,因為上面圖中的四個角色都是需要做集群的。我給出一張官網(wǎng)的架構(gòu)圖,大家嘗試?yán)斫庖幌隆?/p>
其實和我們最開始畫的那張乞丐版的架構(gòu)圖也沒什么區(qū)別,主要是一些細(xì)節(jié)上的差別,聽我細(xì)細(xì)道來。
在上面的技術(shù)架構(gòu)介紹中,我們已經(jīng)知道了 RocketMQ 在主題上是無序的、它只有在隊列層面才是保證有序的。這又扯到兩個概念——普通順序和嚴(yán)格順序。所謂普通順序是指消費(fèi)者通過同一個消費(fèi)隊列收到的消息是有順序的,不同消息隊列收到的消息則可能是無順序的。普通順序消息在 Broker 重啟情況下不會保證消息順序性(短暫時間)。
所謂嚴(yán)格順序是指消費(fèi)者收到的所有消息均是有順序的。嚴(yán)格順序消息即使在異常情況下也會保證消息的順序性。但是,嚴(yán)格順序看起來雖好,實現(xiàn)它可會付出巨大的代價。如果你使用嚴(yán)格順序模式,Broker 集群中只要有一臺機(jī)器不可用,則整個集群都不可用。你還用啥?現(xiàn)在主要場景也就在 binlog 同步。一般而言,我們的 MQ 都是能容忍短暫的亂序,所以推薦使用普通順序模式。(這個嚴(yán)格順序,感覺沒太懂,后面再查一下相關(guān)資料。。。)
那么,我們現(xiàn)在使用了普通順序模式,我們從上面學(xué)習(xí)知道了在 Producer 生產(chǎn)消息的時候會進(jìn)行輪詢(取決你的負(fù)載均衡策略)來向同一主題的不同消息隊列發(fā)送消息。那么如果此時我有幾個消息分別是同一個訂單的創(chuàng)建、支付、發(fā)貨,在輪詢的策略下這三個消息會被發(fā)送到不同隊列,因為在不同的隊列此時就無法使用 RocketMQ 帶來的隊列有序特性來保證消息有序性了。
那么,怎么解決呢?其實很簡單,我們需要處理的僅僅是將同一語義下的消息放入同一個隊列(比如這里是同一個訂單),那我們就可以使用 Hash 取模法來保證同一個訂單在同一個隊列中就行了。
就兩個字——冪等。在編程中一個冪等操作的特點(diǎn)是其任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。比如說,這個時候我們有一個訂單的處理積分的系統(tǒng),每當(dāng)來一個消息的時候它就負(fù)責(zé)為創(chuàng)建這個訂單的用戶的積分加上相應(yīng)的數(shù)值。可是有一次,消息隊列發(fā)送給訂單系統(tǒng) FrancisQ 的訂單信息,其要求是給 FrancisQ 的積分加上 500。但是積分系統(tǒng)在收到 FrancisQ 的訂單信息處理完成之后返回給消息隊列處理成功的信息的時候出現(xiàn)了網(wǎng)絡(luò)波動(當(dāng)然還有很多種情況,比如 Broker 意外重啟等等),這條回應(yīng)沒有發(fā)送成功。
那么,消息隊列沒收到積分系統(tǒng)的回應(yīng)會不會嘗試重發(fā)這個消息?問題就來了,我再發(fā)這個消息,萬一它又給 FrancisQ 的賬戶加上 500 積分怎么辦呢?所以我們需要給我們的消費(fèi)者實現(xiàn)冪等,也就是對同一個消息的處理結(jié)果,執(zhí)行多少次都不變。
那么如何給業(yè)務(wù)實現(xiàn)冪等呢?這個還是需要結(jié)合具體的業(yè)務(wù)的。你可以使用寫入 Redis 來保證,因為 Redis 的 key 和 value 就是天然支持冪等的。當(dāng)然還有使用數(shù)據(jù)庫插入法,基于數(shù)據(jù)庫的唯一鍵來保證重復(fù)數(shù)據(jù)不會被插入多條。不過最主要的還是需要根據(jù)特定場景使用特定的解決方案,你要知道你的消息消費(fèi)是否是完全不可重復(fù)消費(fèi)還是可以忍受重復(fù)消費(fèi)的,然后再選擇強(qiáng)校驗和弱校驗的方式。畢竟在 CS 領(lǐng)域還是很少有技術(shù)銀彈的說法。
簡單了來說,冪等的校驗,還是需要業(yè)務(wù)方來支持,因為你解決不了網(wǎng)絡(luò)抖動問題哈~~
如何解釋分布式事務(wù)呢?事務(wù)大家都知道吧?要么都執(zhí)行要么都不執(zhí)行。在同一個系統(tǒng)中我們可以輕松地實現(xiàn)事務(wù),但是在分布式架構(gòu)中,我們有很多服務(wù)是部署在不同系統(tǒng)之間的,而不同服務(wù)之間又需要進(jìn)行調(diào)用。比如此時我下訂單然后增加積分,如果保證不了分布式事務(wù)的話,就會出現(xiàn)A系統(tǒng)下了訂單,但是B系統(tǒng)增加積分失敗或者A系統(tǒng)沒有下訂單,B系統(tǒng)卻增加了積分。前者對用戶不友好,后者對運(yùn)營商不利,這是我們都不愿意見到的。那么,如何去解決這個問題呢?
如今比較常見的分布式事務(wù)實現(xiàn)有 2PC、TCC 和事務(wù)消息(half 半消息機(jī)制)。每一種實現(xiàn)都有其特定的使用場景,但是也有各自的問題,都不是完美的解決方案。在 RocketMQ 中使用的是事務(wù)消息加上事務(wù)反查機(jī)制來解決分布式事務(wù)問題的。
消息中間件的主要功能是異步解耦,還有個重要功能是擋住前端的數(shù)據(jù)洪峰,保證后端系統(tǒng)的穩(wěn)定性,這就要求消息中間件具有一定的消息堆積能力,消息堆積分以下兩種情況:
評估消息堆積能力主要有以下四點(diǎn):
簡單來說,RocketMQ支持大量消息堆積,消息會存在內(nèi)存,超出內(nèi)存的消息會持久化到磁盤中。
定時消息是指消息發(fā)到Broker后,不能立刻被Consumer消費(fèi),要到特定的時間點(diǎn)或者等待特定的時間后才能被消費(fèi)。如果要支持任意的時間精度,在Broker層面,必須要做消息排序,如果再涉及到持久化,那么消息排序要不可避免的產(chǎn)生巨大性能開銷。RocketMQ支持定時消息,但是不支持任意時間精度,支持特定的level,例如定時5s,10s,1m等。
簡單來說,RocketMQ支持定時消息,但是只支持固定時間,不支持任意精度時間。
上面我講了那么多的 RocketMQ 的架構(gòu)和設(shè)計原理,你有沒有好奇,在 Topic 中的隊列是以什么樣的形式存在的?隊列中的消息又是如何進(jìn)行存儲持久化的呢?我在上文中提到的同步刷盤和異步刷盤又是什么呢?它們會給持久化帶來什么樣的影響呢?下面我將給你們一一解釋。
如上圖所示,在同步刷盤中需要等待一個刷盤成功的 ACK,同步刷盤對 MQ 消息可靠性來說是一種不錯的保障,但是性能上會有較大影響,一般地適用于金融等特定業(yè)務(wù)場景。而異步刷盤往往是開啟一個線程去異步地執(zhí)行刷盤操作。消息刷盤采用后臺異步線程提交的方式進(jìn)行,降低了讀寫延遲,提高了 MQ 的性能和吞吐量,一般適用于如發(fā)驗證碼等對于消息保證要求不太高的業(yè)務(wù)場景。一般地,異步刷盤只有在 Broker 意外宕機(jī)的時候會丟失部分?jǐn)?shù)據(jù),你可以設(shè)置 Broker 的參數(shù) FlushDiskType 來調(diào)整你的刷盤策略(ASYNC_FLUSH 或者 SYNC_FLUSH)。
簡單來說,同步刷盤是刷盤后請求再返回,異步刷盤是直接返回請求,再去慢慢刷盤,可能會導(dǎo)致數(shù)據(jù)丟失。
上面的同步刷盤和異步刷盤是在單個結(jié)點(diǎn)層面的,而同步復(fù)制和異步復(fù)制主要是指的 Borker 主從模式下,主節(jié)點(diǎn)返回消息給客戶端的時候是否需要同步從節(jié)點(diǎn)。
擴(kuò)展知識1:在單主從架構(gòu)中,如果一個主節(jié)點(diǎn)掛掉了,那么也就意味著整個系統(tǒng)不能再生產(chǎn)了。那么這個可用性的問題能否解決呢?一個主從不行那就多個主從的唄,別忘了在我們最初的架構(gòu)圖中,每個 Topic 是分布在不同 Broker 中的。但是這種復(fù)制方式同樣也會帶來一個問題,那就是無法保證嚴(yán)格順序。在上文中我們提到了如何保證的消息順序性是通過將一個語義的消息發(fā)送在同一個隊列中,使用 Topic 下的隊列來保證順序性的。如果此時我們主節(jié)點(diǎn) A 負(fù)責(zé)的是訂單 A 的一系列語義消息,然后它掛了,這樣其他節(jié)點(diǎn)是無法代替主節(jié)點(diǎn)A的,如果我們?nèi)我夤?jié)點(diǎn)都可以存入任何消息,那就沒有順序性可言了。(這點(diǎn)我并不認(rèn)同,我理解主從的對列信息應(yīng)該是一樣的,我從主節(jié)點(diǎn)讀到哪里,如果主節(jié)點(diǎn)掛掉,應(yīng)該是可以到從結(jié)點(diǎn)去讀取的,如果不能這樣,搞個主從就完全沒有意義了。因為主從的信息是一樣的,對隊列的順序是內(nèi)有影響的,我不可能把不同的信息,搞兩個隊列,分別放到主從機(jī)器。)
擴(kuò)展知識2:在 RocketMQ 中采用了 Dledger 解決主從數(shù)據(jù)同步問題。他要求在寫入消息的時候,要求至少消息復(fù)制到半數(shù)以上的節(jié)點(diǎn)之后,才給客?端返回寫?成功,并且它是?持通過選舉來動態(tài)切換主節(jié)點(diǎn)的。這里我就不展開說明了,讀者可以自己去了解。也不是說 Dledger 是個完美的方案,至少在 Dledger 選舉過程中是無法提供服務(wù)的,而且他必須要使用三個節(jié)點(diǎn)或以上,如果多數(shù)節(jié)點(diǎn)同時掛掉他也是無法保證可用性的,而且要求消息復(fù)制板書以上節(jié)點(diǎn)的效率和直接異步復(fù)制還是有一定的差距的。
這個機(jī)制,感覺就像大眾化的版本,基本思路都一樣,為了保證數(shù)據(jù)可用性,我還是推薦同步復(fù)制,當(dāng)大多數(shù)節(jié)點(diǎn)復(fù)制成功,就認(rèn)為復(fù)制完畢,和ETCD的Raft協(xié)議的日志同步原理一樣。
在實際使用RocketMQ的時候我們并不能保證每次發(fā)送的消息都剛好能被消費(fèi)者一次性正常消費(fèi)成功,可能會存在需要多次消費(fèi)才能成功或者一直消費(fèi)失敗的情況,那作為發(fā)送者該做如何處理呢?
RocketMQ提供了ack機(jī)制,以保證消息能夠被正常消費(fèi)。發(fā)送者為了保證消息肯定消費(fèi)成功,只有使用方明確表示消費(fèi)成功,RocketMQ才會認(rèn)為消息消費(fèi)成功。中途斷電,拋出異常等都不會認(rèn)為成功——即都會重新投遞。當(dāng)然,如果消費(fèi)者不告知發(fā)送者我這邊消費(fèi)信息異常,那么發(fā)送者是不會知道的,所以消費(fèi)者在設(shè)置監(jiān)聽的時候需要給個回調(diào)。
為了保證消息是肯定被至少消費(fèi)成功一次,RocketMQ會把這批消息重發(fā)回Broker(topic不是原topic而是這個消費(fèi)租的RETRY topic),在延遲的某個時間點(diǎn)(默認(rèn)是10秒,業(yè)務(wù)可設(shè)置)后,再次投遞到這個ConsumerGroup。而如果一直這樣重復(fù)消費(fèi)都持續(xù)失敗到一定次數(shù)(默認(rèn)16次),就會投遞到DLQ死信隊列。應(yīng)用可以監(jiān)控死信隊列來做人工干預(yù)。
簡單來說,通過ACK保證消息一定能正常消費(fèi),對于異常消息,會重新放回Broker,但是這樣就會打亂消息的順序,所以容錯機(jī)制和消息嚴(yán)格順序,魚和熊掌不可兼得。
這里才是內(nèi)容的重點(diǎn),不僅需要知道RocketMQ的特性,還需要知道支持這些特性的原因:
我們也不能天天去背八股,還需要實踐,RabbitMQ的實操實例,直接看這篇《入門RabbitMQ,這一篇絕對夠!》