一文了解消息隊列

前言

消息隊列一直是Java 後端 大數據經常使用的一個重點,不管是新人還是技術專家 都會或多或少的接觸和使用過一兩個消息隊列(Message Queue)。這篇文章,結合南國最近的一些學習收穫,總結下消息隊列的知識點。

一、爲什麼使用消息隊列?消息隊列有什麼優點和缺點?Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什麼優點和缺點?

面試中如果問到這個問題,其實面試官主要是想看看:

  • 第一,你知不知道你們系統裏爲什麼要用消息隊列這個東西?
    不少候選人,說自己項目裏用了 Redis、MQ,但是其實他並不知道自己爲什麼要用這個東西。
    其實說白了,就是爲了用而用,或者是別人設計的架構,他從頭到尾都沒思考過。
    沒有對自己的架構問過爲什麼的人,一定是平時沒有思考的人,面試官對這類候選人印象通常很不好。
    因爲面試官擔心你進了團隊之後只會木頭木腦的幹呆活兒,不會自己思考。
  • 第二,你既然用了消息隊列這個東西,你知不知道用了有什麼好處&壞處?
    你要是沒考慮過這個,那你盲目弄個 MQ 進系統裏,後面出了問題你是不是就自己溜了給公司留坑?
    你要是沒考慮過引入一個技術可能存在的弊端和風險,面試官把這類候選人招進來了,基本可能就是挖坑型選手。
    就怕你幹 1 年挖一堆坑,自己跳槽了,給公司留下無窮後患。
  • 第三,既然你用了 MQ,可能是某一種 MQ,那麼你當時做沒做過調研?
    你別傻乎乎的自己拍腦袋看個人喜好就瞎用了一個 MQ,比如 Kafka,甚至都從沒調研過業界流行的 MQ 到底有哪幾種。
    每一個 MQ 的優點和缺點是什麼。
    每一個 MQ 沒有絕對的好壞,但是就是看用在哪個場景可以揚長避短,利用其優勢,規避其劣勢。
    如果是一個不考慮技術選型的候選人招進了團隊,leader 交給他一個任務,去設計個什麼系統,他在裏面用一些技術,可能都沒考慮過選型,最後選的技術可能並不一定合適,一樣是留坑。

爲什麼使用消息隊列?

其實就是問問你消息隊列都有哪些使用場景,然後你項目裏具體是什麼場景,說說你在這個場景裏用消息隊列是什麼?

面試官問你這個問題,期望的一個回答是說,你們公司有個什麼業務場景,這個業務場景有個什麼技術挑戰,如果不用 MQ 可能會很麻煩,但是你現在用了 MQ 之後帶給了你很多的好處。

先說一下消息隊列常見的使用場景吧,其實場景有很多,但是比較核心的有 3 個:解耦、異步、削峯。

解耦

看這麼個場景。A 系統發送數據到 BCD 三個系統,通過接口調用發送。如果 E 系統也要這個數據呢?那如果 C 系統現在不需要了呢?A 系統負責人幾乎崩潰…
在這裏插入圖片描述
在這個場景中,A 系統跟其它各種亂七八糟的系統嚴重耦合,A 系統產生一條比較關鍵的數據,很多系統都需要 A 系統將這個數據發送過來。A 系統要時時刻刻考慮 BCDE 四個系統如果掛了該咋辦?要不要重發,要不要把消息存起來?頭髮都白了啊!

如果使用 MQ,A 系統產生一條數據,發送到 MQ 裏面去,哪個系統需要數據自己去 MQ 裏面消費。如果新系統需要數據,直接從 MQ 裏消費即可;如果某個系統不需要這條數據了,就取消對 MQ 消息的消費即可。這樣下來,A 系統壓根兒不需要去考慮要給誰發送數據,不需要維護這個代碼,也不需要考慮人家是否調用成功、失敗超時等情況。
在這裏插入圖片描述
總結:通過一個 MQ,Pub/Sub 發佈訂閱消息這麼一個模型,A 系統就跟其它系統徹底解耦了。

面試技巧:你需要去考慮一下你負責的系統中是否有類似的場景,就是一個系統或者一個模塊,調用了多個系統或者模塊,互相之間的調用很複雜,維護起來很麻煩。但是其實這個調用是不需要直接同步調用接口的,如果用 MQ 給它異步化解耦,也是可以的,你就需要去考慮在你的項目裏,是不是可以運用這個 MQ 去進行系統的解耦。在簡歷中體現出來這塊東西,用 MQ 作解耦。

異步

再來看一個場景,A 系統接收一個請求,需要在自己本地寫庫,還需要在 BCD 三個系統寫庫,自己本地寫庫要 3ms,BCD 三個系統分別寫庫要 300ms、450ms、200ms。最終請求總延時是 3 + 300 + 450 + 200 = 953ms,接近 1s,用戶感覺搞個什麼東西,慢死了慢死了。用戶通過瀏覽器發起請求,等待個 1s,這幾乎是不可接受的。
在這裏插入圖片描述
一般互聯網類的企業,對於用戶直接的操作,一般要求是每個請求都必須在 200 ms 以內完成,對用戶幾乎是無感知的。

如果使用 MQ,那麼 A 系統連續發送 3 條消息到 MQ 隊列中,假如耗時 5ms,A 系統從接受一個請求到返回響應給用戶,總時長是 3 + 5 = 8ms,對於用戶而言,其實感覺上就是點個按鈕,8ms 以後就直接返回了,爽!網站做得真好,真快!
在這裏插入圖片描述

削峯

每天 0:00 到 12:00,A 系統風平浪靜,每秒併發請求數量就 50 個。結果每次一到 12:00 ~ 13:00 ,每秒併發請求數量突然會暴增到 5k+ 條。但是系統是直接基於 MySQL 的,大量的請求湧入 MySQL,每秒鐘對 MySQL 執行約 5k 條 SQL。

一般的 MySQL,扛到每秒 2k 個請求就差不多了,如果每秒請求到 5k 的話,可能就直接把 MySQL 給打死了,導致系統崩潰,用戶也就沒法再使用系統了。

但是高峯期一過,到了下午的時候,就成了低峯期,可能也就 1w 的用戶同時在網站上操作,每秒中的請求數量可能也就 50 個請求,對整個系統幾乎沒有任何的壓力。
在這裏插入圖片描述
如果使用 MQ,每秒 5k 個請求寫入 MQ,A 系統每秒鐘最多處理 2k 個請求,因爲 MySQL 每秒鐘最多處理 2k 個。A 系統從 MQ 中慢慢拉取請求,每秒鐘就拉取 2k 個請求,不要超過自己每秒能處理的最大請求數量就 ok,這樣下來,哪怕是高峯期的時候,A 系統也絕對不會掛掉。而 MQ 每秒鐘 5k 個請求進來,就 2k 個請求出去,結果就導致在中午高峯期(1 個小時),可能有幾十萬甚至幾百萬的請求積壓在 MQ 中。
在這裏插入圖片描述
這個短暫的高峯期積壓是 ok 的,因爲高峯期過了之後,每秒鐘就 50 個請求進 MQ,但是 A 系統依然會按照每秒 2k 個請求的速度在處理。所以說,只要高峯期一過,A 系統就會快速將積壓的消息給解決掉。

消息隊列有什麼優缺點

優點上面已經說了,就是在特殊場景下有其對應的好處,解耦、異步、削峯

缺點有以下幾個:

  • 系統可用性降低
    系統引入的外部依賴越多,越容易掛掉。
    本來你就是 A 系統調用 BCD 三個系統的接口就好了,ABCD 四個系統還好好的,沒啥問題,你偏加個 MQ 進來,萬一 MQ 掛了咋整?
    MQ 一掛,整套系統崩潰,你不就完了?
    如何保證消息隊列的高可用,可以點擊這裏查看。

  • 系統複雜度提高
    硬生生加個 MQ 進來,你怎麼保證消息沒有重複消費?
    怎麼處理消息丟失的情況?
    怎麼保證消息傳遞的順序性?
    頭大頭大,問題一大堆,痛苦不已。

  • 一致性問題
    A 系統處理完了直接返回成功了,人都以爲你這個請求就成功了;
    但是問題是,要是 BCD 三個系統那裏,BD 兩個系統寫庫成功了,結果 C 系統寫庫失敗了,咋整?
    你這數據就不一致了。
    在這裏插入圖片描述
    所以消息隊列實際是一種非常複雜的架構,你引入它有很多好處,但是也得針對它帶來的壞處做各種額外的技術方案和架構來規避掉,做好之後,你會發現,媽呀,系統複雜度提升了一個數量級,也許是複雜了 10 倍。但是關鍵時刻,用,還是得用的。

Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什麼優缺點?

在這裏插入圖片描述
綜上,各種對比之後,有如下建議:
一般的業務系統要引入 MQ,最早大家都用 ActiveMQ,但是現在確實大家用的不多了,沒經過大規模吞吐量場景的驗證,社區也不是很活躍,所以大家還是算了吧,我個人不推薦用這個了;

後來大家開始用 RabbitMQ,但是確實 erlang 語言阻止了大量的 Java 工程師去深入研究和掌控它,對公司而言,幾乎處於不可控的狀態,但是確實人家是開源的,比較穩定的支持,活躍度也高;

不過現在確實越來越多的公司會去用 RocketMQ,確實很不錯,畢竟是阿里出品,但社區可能有突然黃掉的風險(目前 RocketMQ 已捐給 Apache,但 GitHub 上的活躍度其實不算高)對自己公司技術實力有絕對自信的,推薦用 RocketMQ,否則回去老老實實用 RabbitMQ 吧,人家有活躍的開源社區,絕對不會黃。

所以中小型公司,技術實力較爲一般,技術挑戰不是特別高,用 RabbitMQ 是不錯的選擇;大型公司,基礎架構研發實力較強,用 RocketMQ 是很好的選擇。

如果是大數據領域的實時計算、日誌採集等場景,用 Kafka 是業內標準的,絕對沒問題,社區活躍度很高,絕對不會黃,何況幾乎是全世界這個領域的事實性規範。

二、如何保證消息隊列的高可用?

如果有人問到你 MQ 的知識,高可用是必問的。上一講提到,MQ 會導致系統可用性降低。所以只要你用了 MQ,接下來問的一些要點肯定就是圍繞着 MQ 的那些缺點怎麼來解決了。
要是你傻乎乎的就幹用了一個 MQ,各種問題從來沒考慮過,那你就杯具了,面試官對你的感覺就是,只會簡單使用一些技術,沒任何思考,馬上對你的印象就不太好了。這樣的同學招進來要是做個 20k 薪資以內的普通小弟還湊合,要是做薪資 20k+ 的高工,那就慘了,讓你設計個系統,裏面肯定一堆坑,出了事故公司受損失,團隊一起背鍋。

這個問題這麼問是很好的,因爲不能問你 Kafka 的高可用性怎麼保證?ActiveMQ 的高可用性怎麼保證?一個面試官要是這麼問就顯得很沒水平,人家可能用的就是 RabbitMQ,沒用過 Kafka,你上來問人家 Kafka 幹什麼?這不是擺明了刁難人麼。

所以有水平的面試官,問的是 MQ 的高可用性怎麼保證?這樣就是你用過哪個 MQ,你就說說你對那個 MQ 的高可用性的理解。

RabbitMQ 的高可用性

RabbitMQ 是比較有代表性的,因爲是基於主從(非分佈式)做高可用性的,我們就以 RabbitMQ 爲例子講解第一種 MQ 的高可用性怎麼實現。

RabbitMQ 有三種模式:單機模式、普通集羣模式、鏡像集羣模式

單機模式

單機模式,就是 Demo 級別的,一般就是你本地啓動了玩玩兒的😄,沒人生產用單機模式。

普通集羣模式(無高可用性)

普通集羣模式,意思就是在多臺機器上啓動多個 RabbitMQ 實例,每個機器啓動一個。你創建的 queue,只會放在一個 RabbitMQ 實例上,但是每個實例都同步 queue 的元數據(元數據可以認爲是 queue 的一些配置信息,通過元數據,可以找到 queue 所在實例)。你消費的時候,實際上如果連接到了另外一個實例,那麼那個實例會從 queue 所在實例上拉取數據過來。
在這裏插入圖片描述
在這裏插入圖片描述
這種方式確實很麻煩,也不怎麼好,沒做到所謂的分佈式,就是個普通集羣。因爲這導致你要麼消費者每次隨機連接一個實例然後拉取數據,要麼固定連接那個 queue 所在實例消費數據,前者有數據拉取的開銷,後者導致單實例性能瓶頸。

而且如果那個放 queue 的實例宕機了,會導致接下來其他實例就無法從那個實例拉取,如果你開啓了消息持久化,讓 RabbitMQ 落地存儲消息的話,消息不一定會丟,得等這個實例恢復了,然後纔可以繼續從這個 queue 拉取數據。

所以這個事兒就比較尷尬了,這就沒有什麼所謂的高可用性,這方案主要是提高吞吐量的,就是說讓集羣中多個節點來服務某個 queue 的讀寫操作。

鏡像集羣模式(高可用性)

這種模式,纔是所謂的 RabbitMQ 的高可用模式。跟普通集羣模式不一樣的是,在鏡像集羣模式下,你創建的 queue,無論元數據還是 queue 裏的消息都會存在於多個實例上,就是說,每個 RabbitMQ 節點都有這個 queue 的一個完整鏡像,包含 queue 的全部數據的意思。然後每次你寫消息到 queue 的時候,都會自動把消息同步到多個實例的 queue 上。
在這裏插入圖片描述
在這裏插入圖片描述
那麼如何開啓這個鏡像集羣模式呢?其實很簡單,RabbitMQ 有很好的管理控制檯,就是在後臺新增一個策略,這個策略是鏡像集羣模式的策略,指定的時候是可以要求數據同步到所有節點的,也可以要求同步到指定數量的節點,再次創建 queue 的時候,應用這個策略,就會自動將數據同步到其他的節點上去了。

這樣的話,好處在於,你任何一個機器宕機了,沒事兒,其它機器(節點)還包含了這個 queue 的完整數據,別的 consumer 都可以到其它節點上去消費數據。壞處在於,第一,這個性能開銷也太大了吧,消息需要同步到所有機器上,導致網絡帶寬壓力和消耗很重!第二,這麼玩兒,不是分佈式的,就沒有擴展性可言了,如果某個 queue 負載很重,你加機器,新增的機器也包含了這個 queue 的所有數據,並沒有辦法線性擴展你的 queue。你想,如果這個 queue 的數據量很大,大到這個機器上的容量無法容納了,此時該怎麼辦呢?

Kafka 的高可用性

Kafka 一個最基本的架構認識:由多個 broker 組成,每個 broker 是一個節點;你創建一個 topic,這個 topic 可以劃分爲多個 partition,每個 partition 可以存在於不同的 broker 上,每個 partition 就放一部分數據。

這就是天然的分佈式消息隊列,就是說一個 topic 的數據,是分散放在多個機器上的,每個機器就放一部分數據。

實際上 RabbmitMQ 之類的,並不是分佈式消息隊列,它就是傳統的消息隊列,只不過提供了一些集羣、HA(High Availability, 高可用性) 的機制而已,因爲無論怎麼玩兒,RabbitMQ 一個 queue 的數據都是放在一個節點裏的,鏡像集羣下,也是每個節點都放這個 queue 的完整數據。

Kafka 0.8 以前,是沒有 HA 機制的,就是任何一個 broker 宕機了,那個 broker 上的 partition 就廢了,沒法寫也沒法讀,沒有什麼高可用性可言。

比如說,我們假設創建了一個 topic,指定其 partition 數量是 3 個,分別在三臺機器上。但是,如果第二臺機器宕機了,會導致這個 topic 的 1/3 的數據就丟了,因此這個是做不到高可用的。
在這裏插入圖片描述
Kafka 0.8 以後,提供了 HA 機制,就是 replica(複製品) 副本機制。每個 partition 的數據都會同步到其它機器上,形成自己的多個 replica 副本。所有 replica 會選舉一個 leader 出來,那麼生產和消費都跟這個 leader 打交道,然後其他 replica 就是 follower。寫的時候,leader 會負責把數據同步到所有 follower 上去,讀的時候就直接讀 leader 上的數據即可。只能讀寫 leader?很簡單,要是你可以隨意讀寫每個 follower,那麼就要 care 數據一致性的問題,系統複雜度太高,很容易出問題。Kafka 會均勻地將一個 partition 的所有 replica 分佈在不同的機器上,這樣纔可以提高容錯性。
在這裏插入圖片描述
在這裏插入圖片描述
這麼搞,就有所謂的高可用性了,因爲如果某個 broker 宕機了,沒事兒,那個 broker上面的 partition 在其他機器上都有副本的。如果這個宕機的 broker 上面有某個 partition 的 leader,那麼此時會從 follower 中重新選舉一個新的 leader 出來,大家繼續讀寫那個新的 leader 即可。這就有所謂的高可用性了。

寫數據的時候,生產者就寫 leader,然後 leader 將數據落地寫本地磁盤,接着其他 follower 自己主動從 leader 來 pull 數據。一旦所有 follower 同步好數據了,就會發送 ack 給 leader,leader 收到所有 follower 的 ack 之後,就會返回寫成功的消息給生產者。(當然,這只是其中一種模式,還可以適當調整這個行爲)

消費的時候,只會從 leader 去讀,但是隻有當一個消息已經被所有 follower 都同步成功返回 ack 的時候,這個消息纔會被消費者讀到。

看到這裏,相信你大致明白了 Kafka 是如何保證高可用機制的了,對吧?不至於一無所知,現場還能給面試官畫畫圖。要是遇上面試官確實是 Kafka 高手,深挖了問,那你只能說不好意思,太深入的你沒研究過。

三、如何保證消息不被重複消費?或者說,如何保證消息消費的冪等性?

其實這是很常見的一個問題,這倆問題基本可以連起來問。既然是消費消息,那肯定要考慮會不會重複消費?能不能避免重複消費?或者重複消費了也別造成系統異常可以嗎?這個是 MQ 領域的基本問題,其實本質上還是問你使用消息隊列如何保證冪等性,這個是你架構裏要考慮的一個問題。

回答這個問題,首先你別聽到重複消息這個事兒,就一無所知吧,你先大概說一說可能會有哪些重複消費的問題。

首先,比如 RabbitMQ、RocketMQ、Kafka,都有可能會出現消息重複消費的問題,正常。因爲這問題通常不是 MQ 自己保證的,是由我們開發來保證的。挑一個 Kafka 來舉個例子,說說怎麼重複消費吧。

Kafka 實際上有個 offset 的概念,就是每個消息寫進去,都有一個 offset,代表消息的序號,然後 consumer 消費了數據之後,每隔一段時間(定時定期),會把自己消費過的消息的 offset 提交一下,表示“我已經消費過了,下次我要是重啓啥的,你就讓我繼續從上次消費到的 offset 來繼續消費吧”。

但是凡事總有意外,比如我們之前生產經常遇到的,就是你有時候重啓系統,看你怎麼重啓了,如果碰到點着急的,直接 kill 進程了,再重啓。這會導致 consumer 有些消息處理了,但是沒來得及提交 offset,尷尬了。重啓之後,少數消息會再次消費一次。

舉個栗子。

有這麼個場景。數據 1/2/3 依次進入 kafka,kafka 會給這三條數據每條分配一個 offset,代表這條數據的序號,我們就假設分配的 offset 依次是 152/153/154。消費者從 kafka 去消費的時候,也是按照這個順序去消費。假如當消費者消費了 offset=153 的這條數據,剛準備去提交 offset 到 zookeeper,此時消費者進程被重啓了。那麼此時消費過的數據 1/2 的 offset 並沒有提交,kafka 也就不知道你已經消費了 offset=153 這條數據。那麼重啓之後,消費者會找 kafka 說,嘿,哥兒們,你給我接着把上次我消費到的那個地方後面的數據繼續給我傳遞過來。由於之前的 offset 沒有提交成功,那麼數據 1/2 會再次傳過來,如果此時消費者沒有去重的話,那麼就會導致重複消費。
在這裏插入圖片描述
在這裏插入圖片描述
如果消費者乾的事兒是拿一條數據就往數據庫裏寫一條,會導致說,你可能就把數據 1/2 在數據庫裏插入了 2 次,那麼數據就錯啦。

其實重複消費不可怕,可怕的是你沒考慮到重複消費之後,怎麼保證冪等性。

舉個例子吧。假設你有個系統,消費一條消息就往數據庫裏插入一條數據,要是你一個消息重複兩次,你不就插入了兩條,這數據不就錯了?但是你要是消費到第二次的時候,自己判斷一下是否已經消費過了,若是就直接扔了,這樣不就保留了一條數據,從而保證了數據的正確性。

一條數據重複出現兩次,數據庫裏就只有一條數據,這就保證了系統的冪等性。

冪等性,通俗點說,就一個數據,或者一個請求,給你重複來多次,你得確保對應的數據是不會改變的,不能出錯。

所以第二個問題來了,怎麼保證消息隊列消費的冪等性?

其實還是得結合業務來思考,我這裏給幾個思路:

  • 比如你拿個數據要寫庫,你先根據主鍵查一下,如果這數據都有了,你就別插入了,update 一下好吧。

  • 比如你是寫 Redis,那沒問題了,反正每次都是 set,天然冪等性。

  • 比如你不是上面兩個場景,那做的稍微複雜一點,你需要讓生產者發送每條數據的時候,裏面加一個全局唯一的 id,類似訂單 id 之類的東西,然後你這裏消費到了之後,先根據這個 id 去比如 Redis 裏查一下,之前消費過嗎?
    如果沒有消費過,你就處理,然後這個 id 寫 Redis。
    如果消費過了,那你就別處理了,保證別重複處理相同的消息即可。

  • 比如基於數據庫的唯一鍵來保證重複數據不會重複插入多條。
    因爲有唯一鍵約束了,重複數據插入只會報錯,不會導致數據庫中出現髒數據。
    在這裏插入圖片描述
    在這裏插入圖片描述
    當然,如何保證 MQ 的消費是冪等性的,需要結合具體的業務來看。

四、如何保證消息的可靠性傳輸?或者說,如何處理消息丟失的問題?

這個是肯定的,用 MQ 有個基本原則,就是數據不能多一條,也不能少一條,不能多,就是前面說的重複消費和冪等性問題。不能少,就是說這數據別搞丟了。那這個問題你必須得考慮一下。

如果說你這個是用 MQ 來傳遞非常核心的消息,比如說計費、扣費的一些消息,那必須確保這個 MQ 傳遞過程中絕對不會把計費消息給弄丟。

數據的丟失問題,可能出現在生產者、MQ、消費者中,咱們從 RabbitMQ 和 Kafka 分別來分析一下吧。

RabbitMQ

在這裏插入圖片描述
在這裏插入圖片描述

生產者弄丟了數據

生產者將數據發送到 RabbitMQ 的時候,可能數據就在半路給搞丟了,因爲網絡問題啥的,都有可能。

此時可以選擇用 RabbitMQ 提供的事務功能,就是生產者發送數據之前開啓 RabbitMQ 事務channel.txSelect,然後發送消息,如果消息沒有成功被 RabbitMQ 接收到,那麼生產者會收到異常報錯,此時就可以回滾事務channel.txRollback,然後重試發送消息;如果收到了消息,那麼可以提交事務channel.txCommit。

// 開啓事務
channel.txSelect
try {
    // 這裏發送消息
} catch (Exception e) {
    channel.txRollback

    // 這裏再次重發這條消息
    // 提交事務
channel.txCommit
}

但是問題是,RabbitMQ 事務機制(同步)一搞,基本上吞吐量會下來,因爲太耗性能。

所以一般來說,如果你要確保說寫 RabbitMQ 的消息別丟,可以開啓 confirm 模式,在生產者那裏設置開啓 confirm 模式之後,你每次寫的消息都會分配一個唯一的 id,然後如果寫入了 RabbitMQ 中,RabbitMQ 會給你回傳一個 ack 消息,告訴你說這個消息 ok 了。如果 RabbitMQ 沒能處理這個消息,會回調你的一個 nack 接口,告訴你這個消息接收失敗,你可以重試。而且你可以結合這個機制自己在內存裏維護每個消息 id 的狀態,如果超過一定時間還沒接收到這個消息的回調,那麼你可以重發。

事務機制和 confirm 機制最大的不同在於,事務機制是同步的,你提交一個事務之後會阻塞在那兒,但是 confirm 機制是異步的,你發送個消息之後就可以發送下一個消息,然後那個消息 RabbitMQ 接收了之後會異步回調你的一個接口通知你這個消息接收到了。

所以一般在生產者這塊避免數據丟失,都是用 confirm 機制的。

RabbitMQ 弄丟了數據

就是 RabbitMQ 自己弄丟了數據,這個你必須開啓 RabbitMQ 的持久化,就是消息寫入之後會持久化到磁盤,哪怕是 RabbitMQ 自己掛了,恢復之後會自動讀取之前存儲的數據,一般數據不會丟。除非極其罕見的是,RabbitMQ 還沒持久化,自己就掛了,可能導致少量數據丟失,但是這個概率較小。

設置持久化有兩個步驟:

  • 創建 queue 的時候將其設置爲持久化
    這樣就可以保證 RabbitMQ 持久化 queue 的元數據,但是它是不會持久化 queue 裏的數據的。

  • 在發送消息的時候將消息的 deliveryMode 設置爲 2
    就是將消息設置爲持久化的,此時 RabbitMQ 就會將消息持久化到磁盤上去。

必須要同時設置這兩個持久化纔行,RabbitMQ 哪怕是掛了,再次重啓,也會從磁盤上重啓恢復 queue,恢復這個 queue 裏的數據。

注意,哪怕是你給 RabbitMQ 開啓了持久化機制,也有一種可能,就是這個消息寫到了 RabbitMQ 中,但是還沒來得及持久化到磁盤上,結果不巧,此時 RabbitMQ 掛了,就會導致內存裏的一點點數據丟失。

所以,持久化可以跟生產者那邊的 confirm 機制配合起來,只有消息被持久化到磁盤之後,纔會通知生產者 ack了,所以哪怕是在持久化到磁盤之前,RabbitMQ 掛了,數據丟了,生產者收不到 ack,你也是可以自己重發的。

消費端弄丟了數據

RabbitMQ 如果丟失了數據,主要是因爲你消費的時候,剛消費到,還沒處理,結果進程掛了,比如重啓了,那麼就尷尬了,RabbitMQ 認爲你都消費了,這數據就丟了。

這個時候得用 RabbitMQ 提供的 ack 機制,簡單來說,就是你必須關閉 RabbitMQ 的自動 ack,可以通過一個 api 來調用就行,然後每次你自己代碼裏確保處理完的時候,再在程序裏 ack 一把。這樣的話,如果你還沒處理完,不就沒有 ack 了?那 RabbitMQ 就認爲你還沒處理完,這個時候 RabbitMQ 會把這個消費分配給別的 consumer 去處理,消息是不會丟的。
在這裏插入圖片描述

Kafka

消費端弄丟了數據

唯一可能導致消費者弄丟數據的情況,就是說,你消費到了這個消息,然後消費者那邊自動提交了 offset,讓 Kafka 以爲你已經消費好了這個消息,但其實你纔剛準備處理這個消息,你還沒處理,你自己就掛了,此時這條消息就丟咯。

這不是跟 RabbitMQ 差不多嗎,大家都知道 Kafka 會自動提交 offset,那麼只要關閉自動提交 offset,在處理完之後自己手動提交 offset,就可以保證數據不會丟。但是此時確實還是可能會有重複消費,比如你剛處理完,還沒提交 offset,結果自己掛了,此時肯定會重複消費一次,自己保證冪等性就好了。

生產環境碰到的一個問題,就是說我們的 Kafka 消費者消費到了數據之後是寫到一個內存的 queue 裏先緩衝一下,結果有的時候,你剛把消息寫入內存 queue,然後消費者會自動提交 offset。然後此時我們重啓了系統,就會導致內存 queue 裏還沒來得及處理的數據就丟失了。

Kafka 弄丟了數據

這塊比較常見的一個場景,就是 Kafka 某個 broker 宕機,然後重新選舉 partition 的 leader。大家想想,要是此時其他的 follower 剛好還有些數據沒有同步,結果此時 leader 掛了,然後選舉某個 follower 成 leader 之後,不就少了一些數據?這就丟了一些數據啊。

生產環境也遇到過,我們也是,之前 Kafka 的 leader 機器宕機了,將 follower 切換爲 leader 之後,就會發現說這個數據就丟了。

所以此時一般是要求起碼設置如下 4 個參數:

  • 給 topic 設置 replication.factor 參數:
    這個值必須大於 1,要求每個 partition 必須有至少 2 個副本。

  • 在 Kafka 服務端設置 min.insync.replicas 參數:
    這個值必須大於 1,這個是要求一個 leader 至少感知到有至少一個 follower 還跟自己保持聯繫,沒掉隊,這樣才能確保 leader 掛了還有一個 follower 吧。

  • 在 producer 端設置 acks=all:
    這個是要求每條數據,必須是寫入所有 replica 之後,才能認爲是寫成功了。

  • 在 producer 端設置 retries=MAX(很大很大很大的一個值,無限次重試的意思):
    這個是要求一旦寫入失敗,就無限重試,卡在這裏了。

我們生產環境就是按照上述要求配置的,這樣配置之後,至少在 Kafka broker 端就可以保證在 leader 所在 broker 發生故障,進行 leader 切換時,數據不會丟失。

生產者會不會弄丟數據?

如果按照上述的思路設置了 acks=all,一定不會丟,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之後,才認爲本次寫成功了。如果沒滿足這個條件,生產者會自動不斷的重試,重試無限次。

五、如何保證消息的順序性?

其實這個也是用 MQ 的時候必問的話題,第一看看你了不瞭解順序這個事兒?第二看看你有沒有辦法保證消息是有順序的?這是生產系統中常見的問題。

我舉個例子,我們以前做過一個 mysql binlog 同步的系統,壓力還是非常大的,日同步數據要達到上億,就是說數據從一個 mysql 庫原封不動地同步到另一個 mysql 庫裏面去(mysql -> mysql)。常見的一點在於說比如大數據 team,就需要同步一個 mysql 庫過來,對公司的業務系統的數據做各種複雜的操作。

你在 mysql 裏增刪改一條數據,對應出來了增刪改 3 條 binlog 日誌,接着這三條 binlog 發送到 MQ 裏面,再消費出來依次執行,起碼得保證人家是按照順序來的吧?不然本來是:增加、修改、刪除;你愣是換了順序給執行成刪除、修改、增加,不全錯了麼。

本來這個數據同步過來,應該最後這個數據被刪除了;結果你搞錯了這個順序,最後這個數據保留下來了,數據同步就出錯了。

先看看順序會錯亂的倆場景:
在這裏插入圖片描述
在這裏插入圖片描述
解決方案

RabbitMQ

拆分多個 queue,每個 queue 一個 consumer,就是多一些 queue 而已,確實是麻煩點;或者就一個 queue 但是對應一個 consumer,然後這個 consumer 內部用內存隊列做排隊,然後分發給底層不同的 worker 來處理。
在這裏插入圖片描述
在這裏插入圖片描述

Kafka

  • 一個 topic,一個 partition,一個 consumer,內部單線程消費,單線程吞吐量太低,一般不會用這個。

  • 寫 N 個內存 queue,具有相同 key 的數據都到同一個內存 queue;

  • 然後對於 N 個線程,每個線程分別消費一個內存 queue 即可,這樣就能保證順序性。
    在這裏插入圖片描述
    在這裏插入圖片描述

六、如何解決消息隊列的延時以及過期失效問題?消息隊列滿了以後該怎麼處理?有幾百萬消息持續積壓幾小時,說說怎麼解決?

你看這問法,其實本質針對的場景,都是說,可能你的消費端出了問題,不消費了;或者消費的速度極其慢。接着就坑爹了,可能你的消息隊列集羣的磁盤都快寫滿了,都沒人消費,這個時候怎麼辦?或者是這整個就積壓了幾個小時,你這個時候怎麼辦?或者是你積壓的時間太長了,導致比如 RabbitMQ 設置了消息過期時間後就沒了怎麼辦?

所以就這事兒,其實線上挺常見的,一般不出,一出就是大 case。一般常見於,舉個例子,消費端每次消費之後要寫 mysql,結果 mysql 掛了,消費端 hang 那兒了,不動了;或者是消費端出了個什麼岔子,導致消費速度極其慢。

關於這個事兒,我們一個一個來梳理吧,先假設一個場景,我們現在消費端出故障了,然後大量消息在 mq 裏積壓,現在出事故了,慌了。

大量消息在 mq 裏積壓了幾個小時了還沒解決

幾千萬條數據在 MQ 裏積壓了七八個小時,從下午 4 點多,積壓到了晚上 11 點多。這個是我們真實遇到過的一個場景,確實是線上故障了,這個時候要不然就是修復 consumer 的問題,讓它恢復消費速度,然後傻傻的等待幾個小時消費完畢。這個肯定不能在面試的時候說吧。

一個消費者一秒是 1000 條,一秒 3 個消費者是 3000 條,一分鐘就是 18 萬條。所以如果你積壓了幾百萬到上千萬的數據,即使消費者恢復了,也需要大概 1 小時的時間才能恢復過來。

一般這個時候,只能臨時緊急擴容了,具體操作步驟和思路如下:

  1. 先修復 consumer 的問題,確保其恢復消費速度,然後將現有 consumer 都停掉。

  2. 新建一個 topic,partition 是原來的 10 倍,臨時建立好原先 10 倍的 queue 數量。

  3. 然後寫一個臨時的分發數據的 consumer 程序,這個程序部署上去消費積壓的數據,消費之後不做耗時的處理,直接均勻輪詢寫入臨時建立好的 10 倍數量的 queue。

  4. 接着臨時徵用 10 倍的機器來部署 consumer,每一批 consumer 消費一個臨時 queue 的數據。
    這種做法相當於是臨時將 queue 資源和 consumer 資源擴大 10 倍,以正常的 10 倍速度來消費數據。

  5. 等快速消費完積壓數據之後,得恢復原先部署的架構,重新用原先的 consumer 機器來消費消息。

mq 中的消息過期失效了

假設你用的是 RabbitMQ,RabbtiMQ 是可以設置過期時間的,也就是 TTL。如果消息在 queue 中積壓超過一定的時間就會被 RabbitMQ 給清理掉,這個數據就沒了。那這就是第二個坑了。這就不是說數據會大量積壓在 mq 裏,而是大量的數據會直接搞丟。

這個情況下,就不是說要增加 consumer 消費積壓的消息,因爲實際上沒啥積壓,而是丟了大量的消息。我們可以採取一個方案,就是批量重導,這個我們之前線上也有類似的場景幹過。就是大量積壓的時候,我們當時就直接丟棄數據了,然後等過了高峯期以後,比如大家一起喝咖啡熬夜到晚上12點以後,用戶都睡覺了。這個時候我們就開始寫程序,將丟失的那批數據,寫個臨時程序,一點一點的查出來,然後重新灌入 mq 裏面去,把白天丟的數據給他補回來。也只能是這樣了。

假設 1 萬個訂單積壓在 mq 裏面,沒有處理,其中 1000 個訂單都丟了,你只能手動寫程序把那 1000 個訂單給查出來,手動發到 mq 裏去再補一次。

mq 都快寫滿了

如果消息積壓在 mq 裏,你很長時間都沒有處理掉,此時導致 mq 都快寫滿了,咋辦?這個還有別的辦法嗎?沒有,誰讓你第一個方案執行的太慢了,你臨時寫程序,接入數據來消費,消費一個丟棄一個,都不要了,快速消費掉所有的消息。然後走第二個方案,到了晚上再補數據吧。
在這裏插入圖片描述

七、如果讓你寫一個消息隊列,該如何進行架構設計?說一下你的思路。

其實聊到這個問題,一般面試官要考察兩塊:

  • 你有沒有對某一個消息隊列做過較爲深入的原理的瞭解,或者從整體瞭解把握住一個消息隊列的架構原理。

  • 看看你的設計能力,給你一個常見的系統,就是消息隊列系統,看看你能不能從全局把握一下整體架構設計,給出一些關鍵點出來。

說實話,問類似問題的時候,大部分人基本都會蒙,因爲平時從來沒有思考過類似的問題,大多數人就是平時埋頭用,從來不去思考背後的一些東西。類似的問題,比如,如果讓你來設計一個 Spring 框架你會怎麼做?如果讓你來設計一個 Dubbo 框架你會怎麼做?如果讓你來設計一個 MyBatis 框架你會怎麼做?

其實回答這類問題,說白了,不求你看過那技術的源碼,起碼你要大概知道那個技術的基本原理、核心組成部分、基本架構構成,然後參照一些開源的技術把一個系統設計出來的思路說一下就好。

比如說這個消息隊列系統,我們從以下幾個角度來考慮一下:

  • 首先這個 mq 得支持可伸縮性吧,就是需要的時候快速擴容,就可以增加吞吐量和容量,那怎麼搞?
    設計個分佈式的系統唄,參照一下 kafka 的設計理念,broker -> topic -> partition,每個 partition 放一個機器,就存一部分數據。
    如果現在資源不夠了,簡單啊,給 topic 增加 partition,然後做數據遷移,增加機器,不就可以存放更多數據,提供更高的吞吐量了?

  • 其次你得考慮一下這個 mq 的數據要不要落地磁盤吧?
    那肯定要了,落磁盤才能保證別進程掛了數據就丟了。
    那落磁盤的時候怎麼落啊?
    順序寫,這樣就沒有磁盤隨機讀寫的尋址開銷,磁盤順序讀寫的性能是很高的,這就是 kafka 的思路。

  • 其次你考慮一下你的 mq 的可用性啊?
    這個事兒,具體參考之前可用性那個環節講解的 kafka 的高可用保障機制。
    多副本 -> leader & follower -> broker 掛了重新選舉 leader 即可對外服務。

  • 能不能支持數據 0 丟失啊?
    可以的,參考我們之前說的那個 kafka 數據零丟失方案。

mq 肯定是很複雜的,面試官問你這個問題,其實是個開放題,他就是看看你有沒有從架構角度整體構思和設計的思維以及能力。確實這個問題可以刷掉一大批人,因爲大部分人平時不思考這些東西。

八、對於應用之間的調用,如何選擇rpc還是mq?

兩個系統之間的調用,是選擇rpc呢還是mq,說一下你們系統的選擇吧

比如rpc可以是簡單的spring httpinvoker,但是前提是都是java應用而且都是用spring framework,
可以選擇restful的web service或者是重量級的soap web service,
或者是使用rmi,或者編寫java api的socket程序;
或者使用第三方成熟的soa產品,如alibaba的dubbo
或者使用mq系統,

但是對於兩個應用之間的調用,上述的方法都可以使用,大家是怎麼抉擇的,從哪幾個方面去抉擇的呢?

如果是需要實時返回數據的話,那麼mq可能就用不上場了吧,mq優點在於異步和解耦,發送端需要發送後立即獲取返回信息的場景估計還是rpc比較適合。

轉載自:https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/why-mq.md

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章