RabbitMQ

概述

大家平時也有用到一些消息中間件(MQ),但是可能對于 MQ 的理解僅僅停留在會使用 API 能實(shí)現(xiàn)生產(chǎn)消息、消費(fèi)消息就完事了。可能很多人都沒有對 MQ 的一些問題思考過。

比如,你跳槽面試時,如果面試官看到你簡歷上寫了,熟練掌握消息中間件,那么很可能給你發(fā)起如下 4 個面試連環(huán)炮!

1.為什么要使用 MQ?
2.使用了 MQ 之后有什么優(yōu)缺點(diǎn)?
3.怎么保證 MQ 消息不丟失?
4.怎么保證 MQ 的高可用性?

本 Chat 將通過一些場景,配合著通俗易懂的語言和多張手繪彩圖,討論一下這些問題。

為什么要使用 MQ?

相信大家也聽過這樣的一句話:好的架構(gòu)不是設(shè)計出來的,是演進(jìn)出來的。

這句話在引入 MQ 的場景同樣適用,使用 MQ 必定有其道理,是用來解決實(shí)際問題的。而不是胡亂看見別人用了,我也用著玩兒一下。

其實(shí)使用 MQ 的場景有挺多的,但是比較核心的有 3 個:異步、解耦、削峰填谷。

異步

假設(shè) A 系統(tǒng)接收一個請求,需要在自己本地寫庫執(zhí)行 SQL,接著需要調(diào)用 BCD 三個系統(tǒng)的接口。自己本地寫庫要 3ms,調(diào)用 BCD 三個系統(tǒng)分別要 300ms、450ms、200ms。最終請求總延時是 3 + 300 + 450 + 200 = 953ms,接近 1s,可能用戶會感覺太慢了。
此時整個系統(tǒng)大概是這樣的:


image.png

但是一旦使用了 MQ 之后,系統(tǒng) A 只需要發(fā)送 3 條消息到 MQ 中的 3 個消息隊(duì)列,然后就返回給用戶了,假設(shè)發(fā)送消息到 MQ 中耗時 20ms,那么用戶感知到這個接口的耗時僅僅是 20 + 3 = 23ms,用戶幾乎無感知,倍兒爽。

此時整個系統(tǒng)結(jié)構(gòu)大概是這樣的:



可以看到通過 MQ 的異步功能,可以大大提高接口的性能。

解耦

假設(shè) A 系統(tǒng)在用戶發(fā)生某個操作的時候,需要把用戶提交的數(shù)據(jù)同時推送到 B、C 兩個系統(tǒng)的時候。這個時候負(fù)責(zé) A 系統(tǒng)的哥們想:沒事啊,B、C 兩個系統(tǒng)給我提供一個 Http 接口或者 RPC 接口,我把數(shù)據(jù)推送過去不就完事了嗎。負(fù)責(zé) A 系統(tǒng)的哥們美滋滋。

image.png

一切看起來很美好,但是,隨著業(yè)務(wù)快速迭代,這個時候系統(tǒng) D 和系統(tǒng) E 也想要這個數(shù)據(jù),A 系統(tǒng)的開發(fā)同學(xué)就改咯,在發(fā)送數(shù)據(jù)給 BC 的同時加上一個 E,但是后面麻煩來了。

整個系統(tǒng)好像不止這個數(shù)據(jù)要發(fā)送給 BCD、還有第二、第三個數(shù)據(jù)要發(fā)送給 BCD、甚至有時候又加入了 E、F 等等系統(tǒng)也要這個數(shù)據(jù),有時候可能 B 系統(tǒng)突然又不要這個數(shù)據(jù)了,A 系統(tǒng)改來改去,開發(fā)同學(xué)頭皮發(fā)麻了。

更復(fù)雜的場景是數(shù)據(jù)通過接口傳給其他系統(tǒng)有時候還要考慮重試、超時等一些異常情況,頭發(fā)都白了呀。。。

來感受一下現(xiàn)場:


image.png

在這種情況下,使用 MQ 來解耦是在合適不過了,因?yàn)樨?fù)責(zé) A 系統(tǒng)的哥們只需要把消息扔到 MQ 就行了,其他系統(tǒng)按需來訂閱消息就好了,就算某個系統(tǒng)不需要這個數(shù)據(jù)了,也不會需要 A 系統(tǒng)改動代碼:


image.png

這樣一來,負(fù)責(zé) A 系統(tǒng)的哥們神清氣爽了。

削峰填谷

比如我們的訂單系統(tǒng),在下單的時候就會往數(shù)據(jù)庫寫數(shù)據(jù),但是數(shù)據(jù)庫只能支撐每秒 1000 左右的并發(fā)寫入,并發(fā)量再高就容易宕機(jī)。低峰期在的時候并發(fā)也就 100 多個,但是在高峰期時候,并發(fā)量會突然激增到 5000 以上,這個時候數(shù)據(jù)庫肯定死了。

來感受一下數(shù)據(jù)庫被打死的絕望:


image.png

用了 MQ 之后,消息就被 MQ 保存起來了,然后系統(tǒng)就可以按照自己的消費(fèi)能力來消費(fèi),比如每秒 1000 個數(shù)據(jù),這樣慢慢寫入數(shù)據(jù)庫,這樣就不會打死數(shù)據(jù)庫了:


image.png

至于為什么叫做削峰填谷呢?來看看這個圖
image.png

如果沒有用 MQ 的情況下并發(fā)量高峰期的時候是有一個“頂峰”的,然后高峰期過后又是一個低并發(fā)的“谷”。

但是使用了 MQ 之后,限制消費(fèi)消息的速度為 1000,但是這樣一來,高峰期產(chǎn)生的數(shù)據(jù)勢必會被積壓在 MQ 中,但是高峰就被“削”掉了,但是因?yàn)橄⒎e壓,在高峰期過后的一段時間內(nèi),消費(fèi)消息的速度還是會維持在 1000QPS,直到消費(fèi)完積壓的消息,這就叫做“填谷”。

通過上面的分析,知道了為什么要使用 MQ,以及使用了 MQ 有什么好處。知其所以然,明白了自己的系統(tǒng)為什么要使用 MQ。

就不會出現(xiàn)“我們組長要用 MQ 我們就用了”這樣的事情了。

使用了 MQ 之后有什么優(yōu)缺點(diǎn)?

看到這個問題蒙圈了,用了 MQ 就用了 MQ,優(yōu)點(diǎn)上面已經(jīng)說了,但是這個缺點(diǎn)是啥啊。好像沒啥缺點(diǎn)啊。

如果你這樣想,就大錯特錯了,在設(shè)計系統(tǒng)的過程中,除了要清楚的知道為什么要用這個東西,還要思考一下用了之后有什么壞處。這樣才能心里有底,防范于未然。

接下來我們就討論一下用 MQ 會有什么缺點(diǎn)吧?

系統(tǒng)可用性降低

大家想想一下,上面的說解耦的場景,本來 A 系統(tǒng)的哥們要把系統(tǒng)關(guān)鍵數(shù)據(jù)發(fā)送給 BC 系統(tǒng)的,現(xiàn)在突然加入了一個 MQ 了,BC 系統(tǒng)接收數(shù)據(jù)要通過 MQ 來接收,但是大家有沒有考慮過一個問題,萬一 MQ 掛了怎么辦?這就引出一個問題,加入了 MQ 之后,整體系統(tǒng)的可用性是不是就降低了?因?yàn)槎嗔艘粋€風(fēng)險因素:MQ 可能會掛掉。只要 MQ 掛了,數(shù)據(jù)沒了,系統(tǒng)運(yùn)行就不對了。

系統(tǒng)復(fù)雜度提高

本來我的系統(tǒng)通過接口調(diào)用一下就能完事的,但是加入一個 MQ 之后,需要考慮消息重復(fù)消費(fèi)、消息丟失、甚至消息順序性的問題,為了解決這些問題,有需要引入很多復(fù)雜的機(jī)制,這樣一來是不是系統(tǒng)的復(fù)雜度提高了。

數(shù)據(jù)一致性問題

本來好好的,A 系統(tǒng)調(diào)用 BC 系統(tǒng)接口,如果 BC 系統(tǒng)出錯了,會拋出異常,返回給 A 系統(tǒng),讓 A 系統(tǒng)知道,這樣的話就可以做回滾操作了,但是使用了 MQ 之后,A 系統(tǒng)發(fā)送完消息就完事了,就認(rèn)為成功了。但是好死不死,剛好 C 系統(tǒng)寫數(shù)據(jù)庫的時候失敗了,但是 A 認(rèn)為 C 已經(jīng)成功了?這樣一來數(shù)據(jù)就不一致了。

通過分析引入 MQ 的優(yōu)缺點(diǎn)之后,就明白了使用 MQ 有很多優(yōu)點(diǎn),但是會發(fā)現(xiàn)它帶來的缺點(diǎn)又會需要你做各種額外的系統(tǒng)設(shè)計來彌補(bǔ),最后你可能會發(fā)現(xiàn)整個系統(tǒng)復(fù)雜了好幾倍,所以設(shè)計系統(tǒng)的時候要基于這些考慮做出取舍,很多時候你會發(fā)現(xiàn)該用的還是要用的。

怎么保證 MQ 消息不丟失?

使用了 MQ 之后,還要關(guān)心消息丟失的問題。這里我們挑 RabbitMQ 來說明一下吧。

生產(chǎn)者弄丟了數(shù)據(jù)

RabbitMQ 生產(chǎn)者將數(shù)據(jù)發(fā)送到 rabbitmq 的時候,可能數(shù)據(jù)在網(wǎng)絡(luò)傳輸中搞丟了,這個時候 RabbitMQ 收不到消息,消息就丟了。

RabbitMQ 提供了兩種方式來解決這個問題:

事務(wù)方式:

在生產(chǎn)者發(fā)送消息之前,通過 channel.txSelect 開啟一個事務(wù),接著發(fā)送消息,如果消息沒有成功被 RabbitMQ 接收到,生產(chǎn)者會收到異常,此時就可以進(jìn)行事務(wù)回滾 channel.txRollback 然后重新發(fā)送。假如 RabbitMQ 收到了這個消息,就可以提交事務(wù) channel.txCommit。

但是這樣一來,生產(chǎn)者的吞吐量和性能都會降低很多,現(xiàn)在一般不這么干。

另外一種方式就是通過 confirm 機(jī)制:

這個 confirm 模式是在生產(chǎn)者哪里設(shè)置的,就是每次寫消息的時候會分配一個唯一的 id,然后 RabbitMQ 收到之后會回傳一個 ack,告訴生產(chǎn)者這個消息 ok 了。如果 rabbitmq 沒有處理到這個消息,那么就回調(diào)一個 nack 的接口,這個時候生產(chǎn)者就可以重發(fā)。

事務(wù)機(jī)制和 cnofirm 機(jī)制最大的不同在于事務(wù)機(jī)制是同步的,提交一個事務(wù)之后會阻塞在那兒,但是 confirm 機(jī)制是異步的,發(fā)送一個消息之后就可以發(fā)送下一個消息,然后那個消息 rabbitmq 接收了之后會異步回調(diào)你一個接口通知你這個消息接收到了。

所以一般在生產(chǎn)者這塊避免數(shù)據(jù)丟失,都是用 confirm 機(jī)制的。

Rabbitmq 弄丟了數(shù)據(jù)

RabbitMQ 集群也會弄丟消息,這個問題在官方文檔的教程中也提到過,就是說在消息發(fā)送到 RabbitMQ 之后,默認(rèn)是沒有落地磁盤的,萬一 RabbitMQ 宕機(jī)了,這個時候消息就丟失了。

所以為了解決這個問題,RabbitMQ 提供了一個持久化的機(jī)制,消息寫入之后會持久化到磁盤,哪怕是宕機(jī)了,恢復(fù)之后也會自動恢復(fù)之前存儲的數(shù)據(jù),這樣的機(jī)制可以確保消息不會丟失。

設(shè)置持久化有兩個步驟:

1.第一個是創(chuàng)建 queue 的時候?qū)⑵湓O(shè)置為持久化的,這樣就可以保證 rabbitmq 持久化 queue 的元數(shù)據(jù),但是不會持久化 queue 里的數(shù)據(jù);
3.第二個是發(fā)送消息的時候?qū)⑾⒌?deliveryMode 設(shè)置為 2,就是將消息設(shè)置為持久化的,此時 rabbitmq 就會將消息持久化到磁盤上去。
但是這樣一來可能會有人說:萬一消息發(fā)送到 RabbitMQ 之后,還沒來得及持久化到磁盤就掛掉了,數(shù)據(jù)也丟失了。

對于這個問題,其實(shí)是配合上面的 confirm 機(jī)制一起來保證的,就是在消息持久化到磁盤之后才會給生產(chǎn)者發(fā)送 ack 消息。萬一真的遇到了那種極端的情況,生產(chǎn)者是可以感知到的,此時生產(chǎn)者可以通過重試發(fā)送消息給別的 RabbitMQ 節(jié)點(diǎn)

消費(fèi)端弄丟了數(shù)據(jù)

RabbitMQ 消費(fèi)端弄丟了數(shù)據(jù)的情況是這樣的:在消費(fèi)消息的時候,剛拿到消息,結(jié)果進(jìn)程掛了,這個時候 RabbitMQ 就會認(rèn)為你已經(jīng)消費(fèi)成功了,這條數(shù)據(jù)就丟了。

對于這個問題,要先說明一下 RabbitMQ 消費(fèi)消息的機(jī)制:在消費(fèi)者收到消息的時候,會發(fā)送一個 ack 給 RabbitMQ,告訴 RabbitMQ 這條消息被消費(fèi)到了,這樣 RabbitMQ 就會把消息刪除。

但是默認(rèn)情況下這個發(fā)送 ack 的操作是自動提交的,也就是說消費(fèi)者一收到這個消息就會自動返回 ack 給 RabbitMQ,所以會出現(xiàn)丟消息的問題。

所以針對這個問題的解決方案就是:關(guān)閉 RabbitMQ 消費(fèi)者的自動提交 ack,在消費(fèi)者處理完這條消息之后再手動提交 ack。

這樣即使遇到了上面的情況,RabbitMQ 也不會把這條消息刪除,會在你程序重啟之后,重新下發(fā)這條消息過來。

怎么保證 MQ 的高可用性性?

使用了 MQ 之后,我們肯定是希望 MQ 有可高用特性,因?yàn)椴豢赡芙邮軝C(jī)器宕機(jī)了,就無法收發(fā)消息的情況。

這一塊我們也是基于 RabbitMQ 這種經(jīng)典的 MQ 來說明一下:

RabbitMQ 是比較有代表性的,因?yàn)槭腔谥鲝淖龈呖捎眯缘?,我們就以他為例子講解第一種 MQ 的高可用性怎么實(shí)現(xiàn)。

rabbitmq 有三種模式:單機(jī)模式,普通集群模式,鏡像集群模式

單機(jī)模式

單機(jī)模式就是 demo 級別的,就是說只有一臺機(jī)器部署了一個 RabbitMQ 程序。這個會存在單點(diǎn)問題,宕機(jī)就玩完了,沒什么高可用性可言。一般就是你本地啟動了玩玩兒的,沒人生產(chǎn)用單機(jī)模式。

普通集群模式

這個模式的意思就是在多臺機(jī)器上啟動多個 rabbitmq 實(shí)例。類似的 master-slave 模式一樣。但是創(chuàng)建的 queue,只會放在一個 master rabbtimq 實(shí)例上,其他實(shí)例都同步那個接收消息的 RabbitMQ 元數(shù)據(jù)。

在消費(fèi)消息的時候,如果你連接到的 RabbitMQ 實(shí)例不是存放 Queue 數(shù)據(jù)的實(shí)例,這個時候 RabbitMQ 就會從存放 Queue 數(shù)據(jù)的實(shí)例上拉去數(shù)據(jù),然后返回給客戶端。

總的來說,這種方式有點(diǎn)麻煩,沒有做到真正的分布式,每次消費(fèi)者連接一個實(shí)例后拉取數(shù)據(jù),如果連接到不是存放 queue 數(shù)據(jù)的實(shí)例,這個時候會造成額外的性能開銷。如果從放 Queue 的實(shí)例拉取,會導(dǎo)致單實(shí)例性能瓶頸。

如果放 queue 的實(shí)例宕機(jī)了,會導(dǎo)致其他實(shí)例無法拉取數(shù)據(jù),這個集群都無法消費(fèi)消息了,沒有做到真正的高可用。

所以這個事兒就比較尷尬了,這就沒有什么所謂的高可用性可言了,這方案主要是提高吞吐量的,就是說讓集群中多個節(jié)點(diǎn)來服務(wù)某個 queue 的讀寫操作。

鏡像集群模式

鏡像集群模式才是真正的 rabbitmq 的高可用模式,跟普通集群模式不一樣的是:創(chuàng)建的 queue 無論元數(shù)據(jù)還是 queue 里的消息都會存在于多個實(shí)例上,每次寫消息到 queue 的時候,都會自動把消息到多個實(shí)例的 queue 里進(jìn)行消息同步。

這樣的話任何一個機(jī)器宕機(jī)了別的實(shí)例都可以用提供服務(wù),這樣就做到了真正的高可用了。

但是也存在著不好之處:

1.性能開銷過高,消息需要同步所有機(jī)器,會導(dǎo)致網(wǎng)絡(luò)帶寬壓力和消耗很重
2.擴(kuò)展性低:無法解決某個 queue 數(shù)據(jù)量特別大的情況,導(dǎo)致 queue 無法線性拓展。就算加了機(jī)器,那個機(jī)器也會包含 queue 的所有數(shù)據(jù),queue 的數(shù)據(jù)沒有做到分布式存儲。
對于 RabbitMQ 的高可用一般的做法都是開啟鏡像集群模式,這樣起碼來說做到了高可用,一個節(jié)點(diǎn)宕機(jī)了,其他節(jié)點(diǎn)可以繼續(xù)提供服務(wù)。

總結(jié)

通過本篇文章,分析了對于 MQ 的一些常規(guī)問題:

1.為什么使用 MQ?
2.使用 MQ 有什么優(yōu)缺點(diǎn)
3.如何保證消息不丟失
4.如何保證 MQ 高可用性

但是,這些問題僅僅是使用 MQ 的其中一部分需要考慮的問題,事實(shí)上,還有其他更加復(fù)雜的問題需要我們?nèi)ソ鉀Q,

比如說:如何保證消息的順序性?消息隊(duì)列如何選型?消息積壓問題如何解決?

本文僅僅是針對 RabbitMQ 的場景舉例子。還有其他比較的消息隊(duì)列,比如 RocketMQ、Kafka,不同的 MQ 在面臨上述問題的時候,要根據(jù)他們的原理機(jī)制來做對應(yīng)的處理,這些都是本文沒有顧及的內(nèi)容,將在后面的文章中討論。敬請關(guān)注。

轉(zhuǎn)載于:https://gitbook.cn/books/5d65124b2b27dd24ed390665/index.html
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 什么叫消息隊(duì)列 消息(Message)是指在應(yīng)用間傳送的數(shù)據(jù)。消息可以非常簡單,比如只包含文本字符串,也可以更復(fù)雜...
    lijun_m閱讀 1,416評論 0 1
  • 什么叫消息隊(duì)列? 消息(Message)是指在應(yīng)用間傳送的數(shù)據(jù)。消息可以非常簡單,比如只包含文本字符串,也可以更復(fù)...
    Agile_dev閱讀 2,438評論 0 24
  • 1.簡述RabbitMQ中的幾種Exchange的作用。 答:有四種exchange 1)direct excha...
    久伴_不離閱讀 2,863評論 0 8
  • 關(guān)于消息隊(duì)列,從前年開始斷斷續(xù)續(xù)看了些資料,想寫很久了,但一直沒騰出空,近來分別碰到幾個朋友聊這塊的技術(shù)選型,是時...
    中v中閱讀 2,038評論 0 20
  • 關(guān)于消息隊(duì)列,從前年開始斷斷續(xù)續(xù)看了些資料,想寫很久了,但一直沒騰出空,近來分別碰到幾個朋友聊這塊的技術(shù)選型,是時...
    預(yù)流閱讀 586,628評論 51 787

友情鏈接更多精彩內(nèi)容