有個 xx 需求,我應(yīng)該用 Kafka 還是 RabbitMQ ?
下面我會通過 6 個場景,來對比分析一下 Kafka 和 RabbitMQ 的優(yōu)劣。
一、消息的順序
有這樣一個需求:當(dāng)訂單狀態(tài)變化的時候,把訂單狀態(tài)變化的消息發(fā)送給所有關(guān)心訂單變化的系統(tǒng)。
訂單會有創(chuàng)建成功、待付款、已支付、已發(fā)貨的狀態(tài),狀態(tài)之間是單向流動的。

好,現(xiàn)在我們把訂單狀態(tài)變化消息要發(fā)送給所有關(guān)心訂單狀態(tài)的系統(tǒng)上去,實現(xiàn)方式就是用消息隊列。

在這種業(yè)務(wù)下,我們最想要的是什么?
*消息的順序:對于同一筆訂單來說,狀態(tài)的變化都是有嚴(yán)格的先后順序的。
*吞吐量:像訂單的業(yè)務(wù),我們自然希望訂單越多越好。訂單越多,吞吐量就越大。
**在這種情況下,我們先看看 RabbitMQ 是怎么做的。
首先,對于發(fā)消息,并廣播給多個消費者這種情況,RabbitMQ 會為每個消費者建立一個對應(yīng)的隊列。也就是說,如果有 10 個消費者,RabbitMQ 會建立 10 個對應(yīng)的隊列。然后,當(dāng)一條消息被發(fā)出后,RabbitMQ 會把這條消息復(fù)制 10 份放到這 10 個隊列里。

當(dāng) RabbitMQ 把消息放入到對應(yīng)的隊列后,我們緊接著面臨的問題就是,我們應(yīng)該在系統(tǒng)內(nèi)部啟動多少線程去從消息隊列中獲取消息。
如果只是單線程去獲取消息,那自然沒有什么好說的。但是多線程情況,可能就會有問題了……
RabbitMQ 有這么個特性,它在官方文檔就聲明了自己是不保證多線程消費同一個隊列的消息,一定保證順序的。而不保證的原因,是因為多線程時,當(dāng)一個線程消費消息報錯的時候,RabbitMQ 會把消費失敗的消息再入隊,此時就可能出現(xiàn)亂序的情況。

T0 時刻,隊列中有四條消息 A1、B1、B2、A2。其中 A1、A2 表示訂單 A 的兩個狀態(tài):待付款、已付款。B1、B2 也同理,是訂單 B 的待付款、已付款。
到了 T1 時刻,消息 A1 被線程 1 收到,消息 B1 被線程 2 收到。此時,一切都還正常。
到了 T3 時刻,B1 消費出錯了,同時呢,由于線程 1 處理速度快,又從消息隊列中獲取到了 B2。此時,問題開始出現(xiàn)。
到了 T4 時刻,由于 RabbitMQ 線程消費出錯,可以把消息重新入隊的特性,此時 B1 會被重新放到隊列頭部。所以,如果不湊巧,線程 1 獲取到了 B1,就出現(xiàn)了亂序情況,B2 狀態(tài)明明是 B1 的后續(xù)狀態(tài),卻被提前處理了。
所以,可以看到了,這個場景用 RabbitMQ,出現(xiàn)了三個問題:
*為了實現(xiàn)發(fā)布訂閱功能,從而使用的消息復(fù)制,會降低性能并耗費更多資源
*多個消費者無法嚴(yán)格保證消息順序
*大量的訂單集中在一個隊列,吞吐量受到了限制
那么 Kafka 怎么樣呢?Kafka 正好在這三個問題上,表現(xiàn)的要比 RabbitMQ 要好得多。
首先,Kafka 的發(fā)布訂閱并不會復(fù)制消息,因為 Kafka 的發(fā)布訂閱就是消費者直接去獲取被 Kafka 保存在日志文件中的消息就好。無論是多少消費者,他們只需要主動去找到消息在文件中的位置即可。
其次,Kafka 不會出現(xiàn)消費者出錯后,把消息重新入隊的現(xiàn)象。
最后,Kafka 可以對訂單進(jìn)行分區(qū),把不同訂單分到多個分區(qū)中保存,這樣,吞吐量能更好。
所以,對于這個需求 Kafka 更合適。
二、消息的匹配
我曾經(jīng)做過一套營銷系統(tǒng)。這套系統(tǒng)中有個非常顯著的特點,就是非常復(fù)雜非常靈活地匹配規(guī)則。
比如,要根據(jù)推廣內(nèi)容去匹配不同的方式做宣傳。又比如,要根據(jù)不同的活動去匹配不同的渠道去做分發(fā)。
總之,數(shù)不清的匹配規(guī)則是這套系統(tǒng)中非常重要的一個特點。

首先,先看看 RabbitMQ 的,你會發(fā)現(xiàn) RabbitMQ 是允許在消息中添加 routing_key 或者自定義消息頭,然后通過一些特殊的 Exchange,很簡單的就實現(xiàn)了消息匹配分發(fā)。開發(fā)幾乎不用成本。
而 Kafka 呢?如果你要實現(xiàn)消息匹配,開發(fā)成本高多了。
首先,通過簡單的配置去自動匹配和分發(fā)到合適的消費者端這件事是不可能的。
其次,消費者端必須先把所有消息不管需要不需要,都取出來。然后,再根據(jù)業(yè)務(wù)需求,自己去實現(xiàn)各種精準(zhǔn)和模糊匹配??赡芤驗檫^度的復(fù)雜性,還要引入規(guī)則引擎。
這個場景下 RabbitMQ 扳回一分。
三、消息的超時
在電商業(yè)務(wù)里,有個需求:下單之后,如果用戶在 15 分鐘內(nèi)未支付,則自動取消訂單。
你可能奇怪,這種怎么也會用到消息隊列的?
我來先簡單解釋一下,在單一服務(wù)的系統(tǒng),可以起個定時任務(wù)就搞定了。
但是,在 SOA 或者微服務(wù)架構(gòu)下,這樣做就不行了。因為很多個服務(wù)都關(guān)心是否支付這件事,如果每種服務(wù),都自己實現(xiàn)一套定時任務(wù)的邏輯,既重復(fù),又難以維護(hù)。
在這種情況下,我們往往會做一層抽象:把要執(zhí)行的任務(wù)封裝成消息。當(dāng)時間到了,直接扔到消息隊列里,消息的訂閱者們獲取到消息后,直接執(zhí)行即可。
希望把消息延遲一定時間再處理的,被稱為延遲隊列。
對于訂單取消的這種業(yè)務(wù),我們就會在創(chuàng)建訂單的時候,同時扔一個包含了執(zhí)行任務(wù)信息的消息到延遲隊列,指定15分鐘后,讓訂閱這個隊列的各個消費者,可以收到這個消息。隨后,各個消費者所在的系統(tǒng)就可以去執(zhí)行相關(guān)的掃描訂單的任務(wù)了。

RabbitMQ 和 Kafka 消息隊列如何選?
先看下 RabbitMQ 的。
RabbitMQ 的消息自帶手表,消息中有個 TTL 字段,可以設(shè)置消息在 RabbitMQ 中的存放的時間,超時了會被移送到一個叫死信隊列的地方。
所以,延遲隊列 RabbitMQ 最簡單的實現(xiàn)方式就是設(shè)置 TTL,然后一個消費者去監(jiān)聽死信隊列。當(dāng)消息超時了,監(jiān)聽死信隊列的消費者就收到消息了。
不過,這樣做有個大問題:假設(shè),我們先往隊列放入一條過期時間是 10 秒的 A 消息,再放入一條過期時間是 5 秒的 B 消息。 那么問題來了,B 消息會先于 A 消息進(jìn)入死信隊列嗎?
答案是否定的。B 消息會優(yōu)先遵守隊列的先進(jìn)先出規(guī)則,在 A 消息過期后,和其一起進(jìn)入死信隊列被消費者消費。
在 RabbitMQ 的 3.5.8 版本以后,官方推薦的 rabbitmq delayed message exchange 插件可以解決這個問題。
*用了這個插件,我們在發(fā)送消息的時候,把消息發(fā)往一個特殊的 Exchange。
*同時,在消息頭里指定要延遲的時間。
*收到消息的 Exchange 并不會立即把消息放到隊列里,而是在消息延遲時間到達(dá)后,才會把消息放入。

再看下 Kafka 的:

Kafka 要實現(xiàn)延遲隊列就很麻煩了。
*你先需要把消息先放入一個臨時的 topic。
*然后得自己開發(fā)一個做中轉(zhuǎn)的消費者。讓這個中間的消費者先去把消息從這個臨時的 topic 取出來。
*取出來,這消息還不能馬上處理啊,因為沒到時間呢。也沒法保存在自己的內(nèi)存里,怕崩潰了,消息沒了。所以,就得把沒有到時間的消息存入到數(shù)據(jù)庫里。
存入數(shù)據(jù)庫中的消息需要在時間到了之后再放入到 Kafka 里,以便真正的消費者去執(zhí)行真正的業(yè)務(wù)邏輯。
……
想想就已經(jīng)頭大了,這都快搞成調(diào)度平臺了。再高級點,還要用時間輪算法才能更好更準(zhǔn)確。
這次,RabbitMQ 上那一條條戴手表的消息,才是最好的選擇。
四、消息的保持
在微服務(wù)里,事件溯源模式是經(jīng)常用到的。如果想用消息隊列實現(xiàn),一般是把事件當(dāng)成消息,依次發(fā)送到消息隊列中。
事件溯源有個最經(jīng)典的場景,就是事件的重放。簡單來講就是把系統(tǒng)中某段時間發(fā)生的事件依次取出來再處理。而且,根據(jù)業(yè)務(wù)場景不同,這些事件重放很可能不是一次,更可能是重復(fù) N 次。
假設(shè),我們現(xiàn)在需要一批在線事件重放,去排查一些問題。
RabbitMQ 此時就真的不行了,因為消息被人取出來就被刪除了。想再次被重復(fù)消費?對不起。
而 Kafka 呢,消息會被持久化一個專門的日志文件里。不會因為被消費了就被刪除。
所以,對消息不離不棄的 Kafka 相對用過就拋的 RabbitMQ,請選擇 Kafka。
五、消息的錯誤處理
很多時候,在做記錄數(shù)據(jù)相關(guān)業(yè)務(wù)的時候,Kafka 一般是不二選擇。不過,有時候在記錄數(shù)據(jù)吞吐量不大時,我自己倒是更喜歡用 RabbitMQ。
原因就是 Kafka 有一個我很不喜歡的設(shè)計原則:
當(dāng)單個分區(qū)中的消息一旦出現(xiàn)消費失敗,就只能停止而不是跳過這條失敗的消息繼續(xù)消費后面的消息。即不允許消息空洞。
只要消息出現(xiàn)失敗,不管是 Kafka 自身消息格式的損壞,還是消費者處理出現(xiàn)異常,是不允許跳過消費失敗的消息繼續(xù)往后消費的。
所以,在數(shù)據(jù)統(tǒng)計不要求十分精確的場景下選了 Kafka,一旦出現(xiàn)了消息消費問題,就會發(fā)生項目不可用的情況。這真是徒增煩惱。
而 RabbitMQ 呢,它由于會在消息出問題或者消費錯誤的時候,可以重新入隊或者移動消息到死信隊列,繼續(xù)消費后面的,會省心很多。
壞消息就像群眾中的壞蛋那樣,Kafka 處理這種壞蛋太過殘暴,非得把壞蛋揪出來不行。相對來說,RabbitMQ 就溫柔多了,群眾是群眾,壞蛋是壞蛋,分開處理嘛。
六、消息的吞吐量
Kafka 是每秒幾十萬條消息吞吐,而 RabbitMQ 的吞吐量是每秒幾萬條消息。
其實,在一家公司內(nèi)部,有必須用到 Kafka 那么大吞吐量的項目真的很少。大部分項目,像 RabbitMQ 那樣每秒幾萬的消息吞吐,已經(jīng)非常夠了。
在一些沒那么大吞吐量的項目中引入 Kafka,我覺得就不如引入 RabbitMQ。
為什么呢?
因為 Kafka 為了更好的吞吐量,很大程度上增加了自己的復(fù)雜度。而這些復(fù)雜度對項目來說,就是麻煩,主要體現(xiàn)在兩個方面:
1、配置復(fù)雜、維護(hù)復(fù)雜
Kafka 的參數(shù)配置相對 RabbitMQ 是很復(fù)雜的。比如:磁盤管理相關(guān)參數(shù),集群管理相關(guān)參數(shù),ZooKeeper 交互相關(guān)參數(shù),Topic 級別相關(guān)參數(shù)等,都需要一些思考和調(diào)優(yōu)。
另外,Kafka 本身集群和參與管理集群的 ZooKeeper,這就帶來了更多的維護(hù)成本。Kafka 要用好,你要考慮 JVM,消息持久化,集群本身交互,以及 ZooKeeper 本身和它與 Kafka 之間的可靠和效率。
2、用好,用對存在門檻
Kafka 的 Producer 和 Consumer 本身要用好用對也存在很高的門檻。
比如,Producer 消息可靠性保障、冪等性、事務(wù)消息等,都需要對 KafkaProducer 有深入的了解。
而 Consumer 更不用說了,光是一個日志偏移管理就讓一大堆人掉了不少頭發(fā)。
相對來說,RabbitMQ 就簡單得多。你可能都不用配置什么,直接啟動起來就能很穩(wěn)定可靠地使用了。就算配置,也是寥寥幾個參數(shù)設(shè)置即可。
所以,大家在項目中引入消息隊列的時候,真的要好好考慮下,不要因為大家都鼓吹 Kafka 好,就無腦引入。
總結(jié)
可以看到,如果我們要做消息隊列選型,有兩件事是必須要做好的:
*列出業(yè)務(wù)最重要的幾個特點
*深入到消息隊列的細(xì)節(jié)中去比較
等我們對這些中間件的特點非常熟悉之后,甚至可以把業(yè)務(wù)分解成不同的子業(yè)務(wù),再根據(jù)不同的子業(yè)務(wù)的特征,引入不同的消息隊列,即消息隊列混用。這樣,我們就可能會最大化我們的獲益,最小化我們的成本。
說了這么多,其實還有很多 Kafka 和 RabbitMQ 的比較沒有說,比如二者集群的區(qū)別,占用資源多少的比較等。以后有機(jī)會可以再提提。
總之,期待大家看完這篇文章后,能對 Kafka 和 RabbitMQ 的區(qū)別有了更細(xì)節(jié)性的了解。
最后,分享一個網(wǎng)上的比較全的對比圖:
