介紹RabbitMQ前,有必須先了解一下AMQP協議。AMQP協議是一個高級抽象層消息通信協議,RabbitMQ是AMQP協議的實現。它主要包括以下組件:
1. Server(broker): 接受客戶端連接,實現AMQP消息隊列和路由功能的進程。
2. Virtual Host:其實是一個虛擬概念,類似於權限控制組,一個Virtual Host里面可以有若干個Exchange和Queue,但是權限控制的最小粒度是Virtual Host
3.Exchange:接受生產者發送的消息,並根據Binding規則將消息路由給服務器中的隊列。ExchangeType決定了Exchange路由消息的行為,例如,在RabbitMQ中,ExchangeType有direct、Fanout和Topic三種,不同類型的Exchange路由的行為是不一樣的。
4.Message Queue:消息隊列,用於存儲還未被消費者消費的消息。
5.Message: 由Header和Body組成,Header是由生產者添加的各種屬性的集合,包括Message是否被持久化、由哪個Message Queue接受、優先級是多少等。而Body是真正需要傳輸的APP數據。
6.Binding:Binding聯系了Exchange與Message Queue。Exchange在與多個Message Queue發生Binding后會生成一張路由表,路由表中存儲着Message Queue所需消息的限制條件即Binding Key。當Exchange收到Message時會解析其Header得到Routing Key,Exchange根據Routing Key與Exchange Type將Message路由到Message Queue。Binding Key由Consumer在Binding Exchange與Message Queue時指定,而Routing Key由Producer發送Message時指定,兩者的匹配方式由Exchange Type決定。
7.Connection:連接,對於RabbitMQ而言,其實就是一個位於客戶端和Broker之間的TCP連接。
8.Channel:信道,僅僅創建了客戶端到Broker之間的連接后,客戶端還是不能發送消息的。需要為每一個Connection創建Channel,AMQP協議規定只有通過Channel才能執行AMQP的命令。一個Connection可以包含多個Channel。之所以需要Channel,是因為TCP連接的建立和釋放都是十分昂貴的,如果一個客戶端每一個線程都需要與Broker交互,如果每一個線程都建立一個TCP連接,暫且不考慮TCP連接是否浪費,就算操作系統也無法承受每秒建立如此多的TCP連接。RabbitMQ建議客戶端線程之間不要共用Channel,至少要保證共用Channel的線程發送消息必須是串行的,但是建議盡量共用Connection。
9.Command:AMQP的命令,客戶端通過Command完成與AMQP服務器的交互來實現自身的邏輯。例如在RabbitMQ中,客戶端可以通過publish命令發送消息,txSelect開啟一個事務,txCommit提交一個事務。
在了解了AMQP模型以后,需要簡單介紹一下AMQP的協議棧,AMQP協議本身包括三層:
1. Modle Layer,位於協議最高層,主要定義了一些供客戶端調用的命令,客戶端可以利用這些命令實現自己的業務邏輯,例如,客戶端可以通過queue.declare聲明一個隊列,利用consume命令獲取一個隊列中的消息。
2. Session Layer,主要負責將客戶端的命令發送給服務器,在將服務器端的應答返回給客戶端,主要為客戶端與服務器之間通信提供可靠性、同步機制和錯誤處理。
3. Transport Layer,主要傳輸二進制數據流,提供幀的處理、信道復用、錯誤檢測和數據表示。
從AMQP協議可以看出,MessageQueue、Exchange和Binding構成了AMQP協議的核心,下面我們就圍繞這三個主要組件 從應用使用的角度全面的介紹如何利用Rabbit MQ構建消息隊列以及使用過程中的注意事項。

1. 聲明MessageQueue
在Rabbit MQ中,無論是生產者發送消息還是消費者接受消息,都首先需要聲明一個MessageQueue。這就存在一個問題,是生產者聲明還是消費者聲明呢?要解決這個問題,首先需要明確:
a)消費者是無法訂閱或者獲取不存在的MessageQueue中信息。
b)消息被Exchange接受以后,如果沒有匹配的Queue,則會被丟棄。
在明白了上述兩點以后,就容易理解如果是消費者去聲明Queue,就有可能會出現在聲明Queue之前,生產者已發送的消息被丟棄的隱患。如果應用能夠通過消息重發的機制允許消息丟失,則使用此方案沒有任何問題。但是如果不能接受該方案,這就需要無論是生產者還是消費者,在發送或者接受消息前,都需要去嘗試建立消息隊列。這里有一點需要明確,如果客戶端嘗試建立一個已經存在的消息隊列,Rabbit MQ不會做任何事情,並返回客戶端建立成功的。
如果一個消費者在一個信道中正在監聽某一個隊列的消息,Rabbit MQ是不允許該消費者在同一個channel去聲明其他隊列的。Rabbit MQ中,可以通過queue.declare命令聲明一個隊列,可以設置該隊列以下屬性:
a) Exclusive:排他隊列,如果一個隊列被聲明為排他隊列,該隊列僅對首次聲明它的連接可見,並在連接斷開時自動刪除。這里需要注意三點:其一,排他隊列是基於連接可見的,同一連接的不同信道是可以同時訪問同一個連接創建的排他隊列的。其二,“首次”,如果一個連接已經聲明了一個排他隊列,其他連接是不允許建立同名的排他隊列的,這個與普通隊列不同。其三,即使該隊列是持久化的,一旦連接關閉或者客戶端退出,該排他隊列都會被自動刪除的。這種隊列適用於只限於一個客戶端發送讀取消息的應用場景。
b) Auto-delete:自動刪除,如果該隊列沒有任何訂閱的消費者的話,該隊列會被自動刪除。這種隊列適用於臨時隊列。
c) Durable:持久化,這個會在后面作為專門一個章節討論。
d) 其他選項,例如如果用戶僅僅想查詢某一個隊列是否已存在,如果不存在,不想建立該隊列,仍然可以調用queue.declare,只不過需要將參數passive設為true,傳給queue.declare,如果該隊列已存在,則會返回true;如果不存在,則會返回Error,但是不會創建新的隊列。
2. 生產者發送消息
在AMQP模型中,Exchange是接受生產者消息並將消息路由到消息隊列的關鍵組件。ExchangeType和Binding決定了消息的路由規則。所以生產者想要發送消息,首先必須要聲明一個Exchange和該Exchange對應的Binding。可以通過 ExchangeDeclare和BindingDeclare完成。在Rabbit MQ中,聲明一個Exchange需要三個參數:ExchangeName,ExchangeType和Durable。ExchangeName是該Exchange的名字,該屬性在創建Binding和生產者通過publish推送消息時需要指定。ExchangeType,指Exchange的類型,在RabbitMQ中,有三種類型的Exchange:direct ,fanout和topic,不同的Exchange會表現出不同路由行為。Durable是該Exchange的持久化屬性,這個會在消息持久化章節討論。聲明一個Binding需要提供一個QueueName,ExchangeName和BindingKey。下面我們就分析一下不同的ExchangeType表現出的不同路由規則。
生產者在發送消息時,都需要指定一個RoutingKey和Exchange,Exchange在接到該RoutingKey以后,會判斷該ExchangeType:
a) 如果是Direct類型,則會將消息中的RoutingKey與該Exchange關聯的所有Binding中的BindingKey進行比較,如果相等,則發送到該Binding對應的Queue中。


c)如果是Topic類型,則會按照正則表達式,對RoutingKey與BindingKey進行匹配,如果匹配成功,則發送到對應的Queue中。
3. 消費者訂閱消息
在RabbitMQ中消費者有2種方式獲取隊列中的消息:
a) 一種是通過basic.consume命令,訂閱某一個隊列中的消息,channel會自動在處理完上一條消息之后,接收下一條消息。(同一個channel消息處理是串行的)。除非關閉channel或者取消訂閱,否則客戶端將會一直接收隊列的消息。
b) 另外一種方式是通過basic.get命令主動獲取隊列中的消息,但是絕對不可以通過循環調用basic.get來代替basic.consume,這是因為basic.get RabbitMQ在實際執行的時候,是首先consume某一個隊列,然后檢索第一條消息,然后再取消訂閱。如果是高吞吐率的消費者,最好還是建議使用basic.consume。
如果有多個消費者同時訂閱同一個隊列的話,RabbitMQ是采用循環的方式分發消息的,每一條消息只能被一個訂閱者接收。例如,有隊列Queue,其中ClientA和ClientB都Consume了該隊列,MessageA到達隊列后,被分派到ClientA,ClientA回復服務器收到響應,服務器刪除MessageA;再有一條消息MessageB抵達隊列,服務器根據“循環推送”原則,將消息會發給ClientB,然后收到ClientB的確認后,刪除MessageB;等到再下一條消息時,服務器會再將消息發送給ClientA。
這里我們可以看出,消費者再接到消息以后,都需要給服務器發送一條確認命令,這個即可以在handleDelivery里顯示的調用basic.ack實現,也可以在Consume某個隊列的時候,設置autoACK屬性為true實現。這個ACK僅僅是通知服務器可以安全的刪除該消息,而不是通知生產者,與RPC不同。 如果消費者在接到消息以后還沒來得及返回ACK就斷開了連接,消息服務器會重傳該消息給下一個訂閱者,如果沒有訂閱者就會存儲該消息。
既然RabbitMQ提供了ACK某一個消息的命令,當然也提供了Reject某一個消息的命令。當客戶端發生錯誤,調用basic.reject命令拒絕某一個消息時,可以設置一個requeue的屬性,如果為true,則消息服務器會重傳該消息給下一個訂閱者;如果為false,則會直接刪除該消息。當然,也可以通過ack,讓消息服務器直接刪除該消息並且不會重傳。
4. 持久化:
Rabbit MQ默認是不持久隊列、Exchange、Binding以及隊列中的消息的,這意味着一旦消息服務器重啟,所有已聲明的隊列,Exchange,Binding以及隊列中的消息都會丟失。通過設置Exchange和MessageQueue的durable屬性為true,可以使得隊列和Exchange持久化,但是這還不能使得隊列中的消息持久化,這需要生產者在發送消息的時候,將delivery mode設置為2,只有這3個全部設置完成后,才能保證服務器重啟不會對現有的隊列造成影響。這里需要注意的是,只有durable為true的Exchange和durable為ture的Queues才能綁定,否則在綁定時,RabbitMQ都會拋錯的。持久化會對RabbitMQ的性能造成比較大的影響,可能會下降10倍不止。
5. 事務:
對事務的支持是AMQP協議的一個重要特性。假設當生產者將一個持久化消息發送給服務器時,因為consume命令本身沒有任何Response返回,所以即使服務器崩潰,沒有持久化該消息,生產者也無法獲知該消息已經丟失。如果此時使用事務,即通過txSelect()開啟一個事務,然后發送消息給服務器,然后通過txCommit()提交該事務,即可以保證,如果txCommit()提交了,則該消息一定會持久化,如果txCommit()還未提交即服務器崩潰,則該消息不會服務器就收。當然Rabbit MQ也提供了txRollback()命令用於回滾某一個事務。
6. Confirm機制:
使用事務固然可以保證只有提交的事務,才會被服務器執行。但是這樣同時也將客戶端與消息服務器同步起來,這背離了消息隊列解耦的本質。Rabbit MQ提供了一個更加輕量級的機制來保證生產者可以感知服務器消息是否已被路由到正確的隊列中——Confirm。如果設置channel為confirm狀態,則通過該channel發送的消息都會被分配一個唯一的ID,然后一旦該消息被正確的路由到匹配的隊列中后,服務器會返回給生產者一個Confirm,該Confirm包含該消息的ID,這樣生產者就會知道該消息已被正確分發。對於持久化消息,只有該消息被持久化后,才會返回Confirm。Confirm機制的最大優點在於異步,生產者在發送消息以后,即可繼續執行其他任務。而服務器返回Confirm后,會觸發生產者的回調函數,生產者在回調函數中處理Confirm信息。如果消息服務器發生異常,導致該消息丟失,會返回給生產者一個nack,表示消息已經丟失,這樣生產者就可以通過重發消息,保證消息不丟失。Confirm機制在性能上要比事務優越很多。但是Confirm機制,無法進行回滾,就是一旦服務器崩潰,生產者無法得到Confirm信息,生產者其實本身也不知道該消息吃否已經被持久化,只有繼續重發來保證消息不丟失,但是如果原先已經持久化的消息,並不會被回滾,這樣隊列中就會存在兩條相同的消息,系統需要支持去重。
消息持久化是 RabbitMQ 最為人津津樂道的特性之一, RabbitMQ 能夠在付出最小的性能代價的基礎上實現消息的持久化,最大的奧秘就在於 RabbitMQ 多層消息隊列的設計上。下面,本文就從 MessageQueue 的設計和消息在 MessageQueue 的生命周期兩個方面全面介紹 RabbitMQ 的消息隊列。
RabbitMQ完全實現了AMQP協議,類似於一個郵箱服務。Exchange負責根據ExchangeType和RoutingKey將消息投遞到對應的消息隊列中,消息隊列負責在消費者獲取消息前暫存消息。在RabbitMQ中,MessageQueue主要由兩部分組成,一個為AMQQueue,主要負責實現AMQP協議的邏輯功能。另外一個是用來存儲消息的BackingQueue,本文重點關注的是BackingQueue的設計。
在RabbitMQ中BackingQueue又由5個子隊列組成:Q1、Q2、Delta、Q3和Q4。RabbitMQ中的消息一旦進入隊列,不是固定不變的,它會隨着系統的負載在隊列中不斷流動,消息的狀態不斷發生變化。RabbitMQ中的消息一共有5種狀態:
a)Alpha:消息的內容和消息索引都保存在內存中;
b)Beta:消息內容保存在磁盤上,消息索引保存在內存中;
c)Gamma:消息內容保存在磁盤上,消息索引在磁盤和內存都有;
d)Delta:消息內容和索引都在磁盤上;
注意:對於持久化的消息,消息內容和消息索引都必須先保存到磁盤上,才會處於上述狀態中的一種,而Gamma狀態的消息只有持久化的消息才會有該狀態。
BackingQueue 中的 5 個子隊列中的消息狀態, Q1 和 Q4 對應的是 Alpha 狀態, Q2 和 Q3 是 Beta 狀態, Delta 對應的是 Delta 狀態。上述就是 RabbitMQ 的多層隊列結構的設計,我們可以看出從 Q1 到 Q4 ,基本經歷的是由 RAM 到 DISK,再到 RAM 的設計。這樣的設計的好處就是當隊列負載很高的情況下,能夠通過將一部分消息由磁盤保存來節省內存空間,當負載降低的時候,這部分消息又漸漸回到內存,被消費者獲取,使得整個隊列有很好的彈性。下面我們就來看一下,整個消息隊列的工作流程。

下面我們分析一下由於內存不足引起的消息換出。消息換出的條件是內存中保存的消息數量 + 等待 ACK 的消息的數量 >Target_RAM_Count 。當條件觸發時,系統首先會判斷如果當前進入等待 ACK 的消息的速度大於進入隊列的消息的速度時,會先處理等待 ACK 的消息。步驟基本上 Q1->Q2 或者 Q3 移動,取決於 Delta 隊列是否為空。 Q4->Q3 移動, Q2 和Q3 向 Delta 移動。