[訊息佇列]RabbitMQ

Duancf發表於2024-07-29

RabbitMQ

RabbitMQ 是什麼?
RabbitMQ 是一個在 AMQP(Advanced Message Queuing Protocol )基礎上實現的,可複用的企業訊息系統。它可以用於大型軟體系統各個模組之間的高效通訊,支援高併發,支援可擴充套件。它支援多種客戶端如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP 等,支援 AJAX,持久化,用於在分散式系統中儲存轉發訊息,在易用性、擴充套件性、高可用性等方面表現不俗。

RabbitMQ 是使用 Erlang 編寫的一個開源的訊息佇列,本身支援很多的協議:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它變的非常重量級,更適合於企業級的開發。它同時實現了一個 Broker 構架,這意味著訊息在傳送給客戶端時先在中心佇列排隊,對路由(Routing)、負載均衡(Load balance)或者資料持久化都有很好的支援。

PS:也可能直接問什麼是訊息佇列?訊息佇列就是一個使用佇列來通訊的元件。

RabbitMQ 特點?
可靠性: RabbitMQ 使用一些機制來保證可靠性, 如持久化、傳輸確認及釋出確認等。
靈活的路由 : 在訊息進入佇列之前,透過交換器來路由訊息。對於典型的路由功能, RabbitMQ 己經提供了一些內建的交換器來實現。針對更復雜的路由功能,可以將多個交換器繫結在一起, 也可以透過外掛機制來實現自己的交換器。
擴充套件性: 多個 RabbitMQ 節點可以組成一個叢集,也可以根據實際業務情況動態地擴充套件 叢集中節點。
高可用性 : 佇列可以在叢集中的機器上設定映象,使得在部分節點出現問題的情況下隊 列仍然可用。
多種協議: RabbitMQ 除了原生支援 AMQP 協議,還支援 STOMP, MQTT 等多種訊息 中介軟體協議。
多語言客戶端 :RabbitMQ 幾乎支援所有常用語言,比如 Java、 Python、 Ruby、 PHP、 C#、 JavaScript 等。
管理介面 : RabbitMQ 提供了一個易用的使用者介面,使得使用者可以監控和管理訊息、集 群中的節點等。
外掛機制 : RabbitMQ 提供了許多外掛 , 以實現從多方面進行擴充套件,當然也可以編寫自 己的外掛。
RabbitMQ 核心概念?
RabbitMQ 整體上是一個生產者與消費者模型,主要負責接收、儲存和轉發訊息。可以把訊息傳遞的過程想象成:當你將一個包裹送到郵局,郵局會暫存並最終將郵件透過郵遞員送到收件人的手上,RabbitMQ 就好比由郵局、郵箱和郵遞員組成的一個系統。從計算機術語層面來說,RabbitMQ 模型更像是一種交換機模型。

RabbitMQ 的整體模型架構如下:

圖1-RabbitMQ 的整體模型架構
圖1-RabbitMQ 的整體模型架構
下面我會一一介紹上圖中的一些概念。

Producer(生產者) 和 Consumer(消費者)
Producer(生產者) :生產訊息的一方(郵件投遞者)
Consumer(消費者) :消費訊息的一方(郵件收件人)
訊息一般由 2 部分組成:訊息頭(或者說是標籤 Label)和 訊息體。訊息體也可以稱為 payLoad ,訊息體是不透明的,而訊息頭則由一系列的可選屬性組成,這些屬性包括 routing-key(路由鍵)、priority(相對於其他訊息的優先權)、delivery-mode(指出該訊息可能需要永續性儲存)等。生產者把訊息交由 RabbitMQ 後,RabbitMQ 會根據訊息頭把訊息傳送給感興趣的 Consumer(消費者)。

Exchange(交換器)
在 RabbitMQ 中,訊息並不是直接被投遞到 Queue(訊息佇列) 中的,中間還必須經過 Exchange(交換器) 這一層,Exchange(交換器) 會把我們的訊息分配到對應的 Queue(訊息佇列) 中。

Exchange(交換器) 用來接收生產者傳送的訊息並將這些訊息路由給伺服器中的佇列中,如果路由不到,或許會返回給 Producer(生產者) ,或許會被直接丟棄掉 。這裡可以將 RabbitMQ 中的交換器看作一個簡單的實體。

RabbitMQ 的 Exchange(交換器) 有 4 種型別,不同的型別對應著不同的路由策略:direct(預設),fanout, topic, 和 headers,不同型別的 Exchange 轉發訊息的策略有所區別。這個會在介紹 Exchange Types(交換器型別) 的時候介紹到。

Exchange(交換器) 示意圖如下:

Exchange(交換器) 示意圖
Exchange(交換器) 示意圖
生產者將訊息發給交換器的時候,一般會指定一個 RoutingKey(路由鍵),用來指定這個訊息的路由規則,而這個 RoutingKey 需要與交換器型別和繫結鍵(BindingKey)聯合使用才能最終生效。

RabbitMQ 中透過 Binding(繫結) 將 Exchange(交換器) 與 Queue(訊息佇列) 關聯起來,在繫結的時候一般會指定一個 BindingKey(繫結建) ,這樣 RabbitMQ 就知道如何正確將訊息路由到佇列了,如下圖所示。一個繫結就是基於路由鍵將交換器和訊息佇列連線起來的路由規則,所以可以將交換器理解成一個由繫結構成的路由表。Exchange 和 Queue 的繫結可以是多對多的關係。

Binding(繫結) 示意圖:

Binding(繫結) 示意圖
Binding(繫結) 示意圖
生產者將訊息傳送給交換器時,需要一個 RoutingKey,當 BindingKey 和 RoutingKey 相匹配時,訊息會被路由到對應的佇列中。在繫結多個佇列到同一個交換器的時候,這些繫結允許使用相同的 BindingKey。BindingKey 並不是在所有的情況下都生效,它依賴於交換器型別,比如 fanout 型別的交換器就會無視,而是將訊息路由到所有繫結到該交換器的佇列中。

Queue(訊息佇列)
Queue(訊息佇列) 用來儲存訊息直到傳送給消費者。它是訊息的容器,也是訊息的終點。一個訊息可投入一個或多個佇列。訊息一直在佇列裡面,等待消費者連線到這個佇列將其取走。

RabbitMQ 中訊息只能儲存在 佇列 中,這一點和 Kafka 這種訊息中介軟體相反。Kafka 將訊息儲存在 topic(主題) 這個邏輯層面,而相對應的佇列邏輯只是 topic 實際儲存檔案中的位移標識。 RabbitMQ 的生產者生產訊息並最終投遞到佇列中,消費者可以從佇列中獲取訊息並消費。

多個消費者可以訂閱同一個佇列,這時佇列中的訊息會被平均分攤(Round-Robin,即輪詢)給多個消費者進行處理,而不是每個消費者都收到所有的訊息並處理,這樣避免訊息被重複消費。

RabbitMQ 不支援佇列層面的廣播消費,如果有廣播消費的需求,需要在其上進行二次開發,這樣會很麻煩,不建議這樣做。

Broker(訊息中介軟體的服務節點)
對於 RabbitMQ 來說,一個 RabbitMQ Broker 可以簡單地看作一個 RabbitMQ 服務節點,或者 RabbitMQ 服務例項。大多數情況下也可以將一個 RabbitMQ Broker 看作一臺 RabbitMQ 伺服器。

下圖展示了生產者將訊息存入 RabbitMQ Broker,以及消費者從 Broker 中消費資料的整個流程。

訊息佇列的運轉過程
訊息佇列的運轉過程
這樣圖 1 中的一些關於 RabbitMQ 的基本概念我們就介紹完畢了,下面再來介紹一下 Exchange Types(交換器型別) 。

Exchange Types(交換器型別)
RabbitMQ 常用的 Exchange Type 有 fanout、direct、topic、headers 這四種(AMQP 規範裡還提到兩種 Exchange Type,分別為 system 與 自定義,這裡不予以描述)。

1、fanout

fanout 型別的 Exchange 路由規則非常簡單,它會把所有傳送到該 Exchange 的訊息路由到所有與它繫結的 Queue 中,不需要做任何判斷操作,所以 fanout 型別是所有的交換機型別裡面速度最快的。fanout 型別常用來廣播訊息。

2、direct

direct 型別的 Exchange 路由規則也很簡單,它會把訊息路由到那些 Bindingkey 與 RoutingKey 完全匹配的 Queue 中。

direct 型別交換器
direct 型別交換器
以上圖為例,如果傳送訊息的時候設定路由鍵為“warning”,那麼訊息會路由到 Queue1 和 Queue2。如果在傳送訊息的時候設定路由鍵為"Info”或者"debug”,訊息只會路由到 Queue2。如果以其他的路由鍵傳送訊息,則訊息不會路由到這兩個佇列中。

direct 型別常用在處理有優先順序的任務,根據任務的優先順序把訊息傳送到對應的佇列,這樣可以指派更多的資源去處理高優先順序的佇列。

3、topic

前面講到 direct 型別的交換器路由規則是完全匹配 BindingKey 和 RoutingKey ,但是這種嚴格的匹配方式在很多情況下不能滿足實際業務的需求。topic 型別的交換器在匹配規則上進行了擴充套件,它與 direct 型別的交換器相似,也是將訊息路由到 BindingKey 和 RoutingKey 相匹配的佇列中,但這裡的匹配規則有些不同,它約定:

RoutingKey 為一個點號“.”分隔的字串(被點號“.”分隔開的每一段獨立的字串稱為一個單詞),如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”;
BindingKey 和 RoutingKey 一樣也是點號“.”分隔的字串;
BindingKey 中可以存在兩種特殊字串“”和“#”,用於做模糊匹配,其中“”用於匹配一個單詞,“#”用於匹配多個單詞(可以是零個)。
topic 型別交換器
topic 型別交換器
以上圖為例:

路由鍵為 “com.rabbitmq.client” 的訊息會同時路由到 Queue1 和 Queue2;
路由鍵為 “com.hidden.client” 的訊息只會路由到 Queue2 中;
路由鍵為 “com.hidden.demo” 的訊息只會路由到 Queue2 中;
路由鍵為 “java.rabbitmq.demo” 的訊息只會路由到 Queue1 中;
路由鍵為 “java.util.concurrent” 的訊息將會被丟棄或者返回給生產者(需要設定 mandatory 引數),因為它沒有匹配任何路由鍵。
4、headers(不推薦)

headers 型別的交換器不依賴於路由鍵的匹配規則來路由訊息,而是根據傳送的訊息內容中的 headers 屬性進行匹配。在繫結佇列和交換器時指定一組鍵值對,當傳送訊息到交換器時,RabbitMQ 會獲取到該訊息的 headers(也是一個鍵值對的形式),對比其中的鍵值對是否完全匹配佇列和交換器繫結時指定的鍵值對,如果完全匹配則訊息會路由到該佇列,否則不會路由到該佇列。headers 型別的交換器效能會很差,而且也不實用,基本上不會看到它的存在。

AMQP

RabbitMQ 就是 AMQP 協議的 Erlang 的實現(當然 RabbitMQ 還支援 STOMP2、 MQTT3 等協議 ) AMQP 的模型架構 和 RabbitMQ 的模型架構是一樣的,生產者將訊息傳送給交換器,交換器和佇列繫結 。

RabbitMQ 中的交換器、交換器型別、佇列、繫結、路由鍵等都是遵循的 AMQP 協議中相 應的概念。目前 RabbitMQ 最新版本預設支援的是 AMQP 0-9-1。

AMQP 協議的三層:

Module Layer:協議最高層,主要定義了一些客戶端呼叫的命令,客戶端可以用這些命令實現自己的業務邏輯。
Session Layer:中間層,主要負責客戶端命令傳送給伺服器,再將服務端應答返回客戶端,提供可靠性同步機制和錯誤處理。
TransportLayer:最底層,主要傳輸二進位制資料流,提供幀的處理、通道複用、錯誤檢測和資料表示等。
AMQP 模型的三大元件:

交換器 (Exchange):訊息代理伺服器中用於把訊息路由到佇列的元件。
佇列 (Queue):用來儲存訊息的資料結構,位於硬碟或記憶體中。
繫結 (Binding):一套規則,告知交換器訊息應該將訊息投遞給哪個佇列。
說說生產者 Producer 和消費者 Consumer?
生產者 :

訊息生產者,就是投遞訊息的一方。
訊息一般包含兩個部分:訊息體(payload)和標籤(Label)。
消費者:

消費訊息,也就是接收訊息的一方。
消費者連線到 RabbitMQ 伺服器,並訂閱到佇列上。消費訊息時只消費訊息體,丟棄標籤。
說說 Broker 服務節點、Queue 佇列、Exchange 交換器?
Broker:可以看做 RabbitMQ 的服務節點。一般情況下一個 Broker 可以看做一個 RabbitMQ 伺服器。
Queue:RabbitMQ 的內部物件,用於儲存訊息。多個消費者可以訂閱同一佇列,這時佇列中的訊息會被平攤(輪詢)給多個消費者進行處理。
Exchange:生產者將訊息傳送到交換器,由交換器將訊息路由到一個或者多個佇列中。當路由不到時,或返回給生產者或直接丟棄。
什麼是死信佇列?如何導致的?
DLX,全稱為 Dead-Letter-Exchange,死信交換器,死信郵箱。當訊息在一個佇列中變成死信 (dead message) 之後,它能被重新傳送到另一個交換器中,這個交換器就是 DLX,繫結 DLX 的佇列就稱之為死信佇列。

導致的死信的幾種原因:

訊息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。
訊息 TTL 過期。
佇列滿了,無法再新增。
什麼是延遲佇列?RabbitMQ 怎麼實現延遲佇列?
延遲佇列指的是儲存對應的延遲訊息,訊息被髮送以後,並不想讓消費者立刻拿到訊息,而是等待特定時間後,消費者才能拿到這個訊息進行消費。

RabbitMQ 本身是沒有延遲佇列的,要實現延遲訊息,一般有兩種方式:

透過 RabbitMQ 本身佇列的特性來實現,需要使用 RabbitMQ 的死信交換機(Exchange)和訊息的存活時間 TTL(Time To Live)。
在 RabbitMQ 3.5.7 及以上的版本提供了一個外掛(rabbitmq-delayed-message-exchange)來實現延遲佇列功能。同時,外掛依賴 Erlang/OPT 18.0 及以上。
也就是說,AMQP 協議以及 RabbitMQ 本身沒有直接支援延遲佇列的功能,但是可以透過 TTL 和 DLX 模擬出延遲佇列的功能。

什麼是優先順序佇列?
RabbitMQ 自 V3.5.0 有優先順序佇列實現,優先順序高的佇列會先被消費。

可以透過x-max-priority引數來實現優先順序佇列。不過,當消費速度大於生產速度且 Broker 沒有堆積的情況下,優先順序顯得沒有意義。

RabbitMQ 有哪些工作模式?
簡單模式
work 工作模式
pub/sub 釋出訂閱模式
Routing 路由模式
Topic 主題模式
RabbitMQ 訊息怎麼傳輸?
由於 TCP 連結的建立和銷燬開銷較大,且併發數受系統資源限制,會造成效能瓶頸,所以 RabbitMQ 使用通道的方式來傳輸資料。通道(Channel)是生產者、消費者與 RabbitMQ 通訊的渠道,通道是建立在 TCP 連結上的虛擬連結,且每條 TCP 連結上的通道數量沒有限制。就是說 RabbitMQ 在一條 TCP 連結上建立成百上千個通道來達到多個執行緒處理,這個 TCP 被多個執行緒共享,每個通道在 RabbitMQ 都有唯一的 ID,保證了通道私有性,每個通道對應一個執行緒使用。

如何保證訊息的可靠性?
訊息到 MQ 的過程中搞丟,MQ 自己搞丟,MQ 到消費過程中搞丟。

生產者到 RabbitMQ:事務機制和 Confirm 機制,注意:事務機制和 Confirm 機制是互斥的,兩者不能共存,會導致 RabbitMQ 報錯。
RabbitMQ 自身:持久化、叢集、普通模式、映象模式。
RabbitMQ 到消費者:basicAck 機制、死信佇列、訊息補償機制。
如何保證 RabbitMQ 訊息的順序性?
拆分多個 queue(訊息佇列),每個 queue(訊息佇列) 一個 consumer(消費者),就是多一些 queue (訊息佇列)而已,確實是麻煩點;
或者就一個 queue (訊息佇列)但是對應一個 consumer(消費者),然後這個 consumer(消費者)內部用記憶體佇列做排隊,然後分發給底層不同的 worker 來處理。
如何保證 RabbitMQ 高可用的?
RabbitMQ 是比較有代表性的,因為是基於主從(非分散式)做高可用性的,我們就以 RabbitMQ 為例子講解第一種 MQ 的高可用性怎麼實現。RabbitMQ 有三種模式:單機模式、普通叢集模式、映象叢集模式。

單機模式

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

普通叢集模式

意思就是在多臺機器上啟動多個 RabbitMQ 例項,每個機器啟動一個。你建立的 queue,只會放在一個 RabbitMQ 例項上,但是每個例項都同步 queue 的後設資料(後設資料可以認為是 queue 的一些配置資訊,透過後設資料,可以找到 queue 所在例項)。

你消費的時候,實際上如果連線到了另外一個例項,那麼那個例項會從 queue 所在例項上拉取資料過來。這方案主要是提高吞吐量的,就是說讓叢集中多個節點來服務某個 queue 的讀寫操作。

映象叢集模式

這種模式,才是所謂的 RabbitMQ 的高可用模式。跟普通叢集模式不一樣的是,在映象叢集模式下,你建立的 queue,無論後設資料還是 queue 裡的訊息都會存在於多個例項上,就是說,每個 RabbitMQ 節點都有這個 queue 的一個完整映象,包含 queue 的全部資料的意思。然後每次你寫訊息到 queue 的時候,都會自動把訊息同步到多個例項的 queue 上。RabbitMQ 有很好的管理控制檯,就是在後臺新增一個策略,這個策略是映象叢集模式的策略,指定的時候是可以要求資料同步到所有節點的,也可以要求同步到指定數量的節點,再次建立 queue 的時候,應用這個策略,就會自動將資料同步到其他的節點上去了。

這樣的好處在於,你任何一個機器當機了,沒事兒,其它機器(節點)還包含了這個 queue 的完整資料,別的 consumer 都可以到其它節點上去消費資料。壞處在於,第一,這個效能開銷也太大了吧,訊息需要同步到所有機器上,導致網路頻寬壓力和消耗很重!RabbitMQ 一個 queue 的資料都是放在一個節點裡的,映象叢集下,也是每個節點都放這個 queue 的完整資料。

如何解決訊息佇列的延時以及過期失效問題?
RabbtiMQ 是可以設定過期時間的,也就是 TTL。如果訊息在 queue 中積壓超過一定的時間就會被 RabbitMQ 給清理掉,這個資料就沒了。那這就是第二個坑了。這就不是說資料會大量積壓在 mq 裡,而是大量的資料會直接搞丟。我們可以採取一個方案,就是批次重導,這個我們之前線上也有類似的場景幹過。就是大量積壓的時候,我們當時就直接丟棄資料了,然後等過了高峰期以後,比如大家一起喝咖啡熬夜到晚上 12 點以後,使用者都睡覺了。這個時候我們就開始寫程式,將丟失的那批資料,寫個臨時程式,一點一點的查出來,然後重新灌入 mq 裡面去,把白天丟的資料給他補回來。也只能是這樣了。假設 1 萬個訂單積壓在 mq 裡面,沒有處理,其中 1000 個訂單都丟了,你只能手動寫程式把那 1000 個訂單給查出來,手動發到 mq 裡去再補一次。

相關文章