一、RabbitMQ簡介
1.1 RabbitMQ是什么
RabbitMQ是一個開源的AMQP實現,服務器端用Erlang語言編寫,支持多種客戶端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系統中存儲轉發消息,在易用性、擴展性、高可用性等方面表現不俗。
1.2 RabbitMQ特點
(1)可靠性(Reliability)RabbitMQ 使用一些機制來保證可靠性,如持久化、傳輸確認、發布確認。
(2)靈活的路由(Flexible Routing)在消息進入隊列之前,通過 Exchange 來路由消息的。對于典型的路由功能,RabbitMQ 已經提供了一些內置的 Exchange 來實現。針對更復雜的路由功能,可以將多個 Exchange 綁定在一起,也通過插件機制實現自己的 Exchange 。
(3)消息集群(Clustering)多個 RabbitMQ 服務器可以組成一個集群,形成一個邏輯 Broker 。
(4)高可用(Highly Available Queues)隊列可以在集群中的機器上進行鏡像,使得在部分節點出問題的情況下隊列仍然可用。
(5)多種協議(Multi-protocol)RabbitMQ 支持多種消息隊列協議,比如 STOMP、MQTT 等等。
(6)多語言客戶端(Many Clients)RabbitMQ 幾乎支持所有常用語言,比如 Java、.NET、Ruby 等等。
(7)管理界面(Management UI)RabbitMQ 提供了一個易用的用戶界面,使得用戶可以監控和管理消息 Broker 的許多方面。
(8)跟蹤機制(Tracing)如果消息異常,RabbitMQ 提供了消息跟蹤機制,使用者可以找出發生了什么。
(9)插件機制(Plugin System)RabbitMQ 提供了許多插件,來從多方面進行擴展,也可以編寫自己的插件。
1.3 RabbitMQ 解決什么問題
(1)你是否遇到過兩個(多個)系統間需要通過定時任務來同步某些數據?你是否在為異構系統的不同進程間相互調用、通訊的問題而苦惱、掙扎?
(2)在Web應用高并發環境下,由于來不及同步處理,請求往往會發生堵塞。比如說,大量的insert、update請求同時到達mysql,會帶來無數的行鎖表鎖,最后導致請求數過多,觸發too many connections錯誤。
(3)消息服務擅長于解決多系統、異構系統間的數據交換(消息通知/通訊)問題,你也可以把它用于系統間服務的相互調用(RPC)通過使用消息隊列,我們可以異步處理請求,從而緩解系統的壓力。
1.4 RabbitMQ 應用場景
對于一個大型的軟件系統來說,它會有很多的組件或者說模塊或者說子系統或者(Subsystem or Component or Submodule)。那么這些模塊的如何通信?這和傳統的IPC有很大的區別。傳統的IPC很多都是在單一系統上的,模塊耦合性很大,不適合擴展(Scalability);如果使用socket那么不同的模塊的確可以部署到不同的機器上,但是還是有很多問題需要解決。比如:
(1)信息的發送者和接收者如何維持這個連接,如果一方的連接中斷,這期間的數據如何方式丟失?
(2)如何降低發送者和接收者的耦合度?
(3)如何讓Priority高的接收者先接到數據?
(4)如何做到Load balance?有效均衡接收者的負載?
(5)如何有效的將數據發送到相關的接收者?也就是說將接收者subscribe不同的數據,如何做有效的filter。
(6)如何做到可擴展,甚至將這個通信模塊發到cluster上?
(7)如何保證接收者接收到了完整,正確的數據?
1.5 RabbitMQ 集群方式
(1)普通模式:默認的集群模式。?對于Queue來說,消息實體只存在于其中一個節點,A、B兩個節點僅有相同的元數據,即隊列結構,但隊列的元數據僅保存有一份,即創建該隊列的rabbitmq節點(A節點),當A節點宕機,你可以去其B節點查看,./rabbitmqctl list_queues 發現該隊列已經丟失,但聲明的exchange還存在。
當消息進入A節點的Queue中后,consumer從B節點拉取時,RabbitMQ會臨時在A、B間進行消息傳輸,把A中的消息實體取出并經過B發送給consumer,所以consumer應平均連接每一個節點,從中取消息。
該模式存在一個問題就是當A節點故障后,B節點無法取到A節點中還未消費的消息實體。如果做了隊列持久化或消息持久化,那么得等A節點恢復,然后才可被消費,并且在A節點恢復之前其它節點不能再創建A節點已經創建過的持久隊列;如果沒有持久化的話,消息就會失丟。
這種模式更適合非持久化隊列,只有該隊列是非持久的,客戶端才能重新連接到集群里的其他節點,并重新創建隊列。假如該隊列是持久化的,那么唯一辦法是將故障節點恢復起來。
(2)鏡像模式:把需要的隊列做成鏡像隊列,存在于多個節點。
該模式解決了普通模式的問題,其實質不同之處在于,消息實體會主動在鏡像節點間同步,而不是在consumer取數據時臨時拉取。
該模式帶來的副作用也很明顯,除了降低系統性能外,如果鏡像隊列數量過多,加之大量的消息進入,集群內部的網絡帶寬將會被這種同步通訊大大消耗掉。
所以在對可靠性要求較高的場合中適用,一個隊列想做成鏡像隊列,需要先設置policy,然后客戶端創建隊列的時候,rabbitmq集群根據“隊列名稱”自動設置是普通集群模式或鏡像隊列。
二、RabbitMQ 基本概念
2.1 RabbitMQ 之基本概念
Message消息,消息是不具名的,它由消息頭和消息體組成。消息體是不透明的,而消息頭則由一系列的可選屬性組成,這些屬性包括routing-key(路由鍵)、priority(相對于其他消息的優先權)、delivery-mode(指出該消息可能需要持久性存儲)等。
Publisher消息的生產者,也是一個向交換器發布消息的客戶端應用程序。
Exchange交換器,用來接收生產者發送的消息并將這些消息路由給服務器中的隊列。
Binding綁定,用于消息隊列和交換器之間的關聯。一個綁定就是基于路由鍵將交換器和消息隊列連接起來的路由規則,所以可以將交換器理解成一個由綁定構成的路由表。
Queue消息隊列,用來保存消息直到發送給消費者。它是消息的容器,也是消息的終點。一個消息可投入一個或多個隊列。消息一直在隊列里面,等待消費者連接到這個隊列將其取走。
Connection網絡連接,比如一個TCP連接。
Channel信道,多路復用連接中的一條獨立的雙向數據流通道。信道是建立在真實的TCP連接內地虛擬連接,AMQP 命令都是通過信道發出去的,不管是發布消息、訂閱隊列還是接收消息,這些動作都是通過信道完成。因為對于操作系統來說建立和銷毀 TCP 都是非常昂貴的開銷,所以引入了信道的概念,以復用一條 TCP 連接。
Consumer消息的消費者,表示一個從消息隊列中取得消息的客戶端應用程序。
Virtual Host虛擬主機,表示一批交換器、消息隊列和相關對象。虛擬主機是共享相同的身份認證和加密環境的獨立服務器域。每個 vhost 本質上就是一個 mini 版的 RabbitMQ 服務器,擁有自己的隊列、交換器、綁定和權限機制。vhost 是 AMQP 概念的基礎,必須在連接時指定,RabbitMQ 默認的 vhost 是 / 。
Broker表示消息隊列服務器實體。
2.2 RabbitMQ 之AMPQ
AMQP,即Advanced Message Queuing Protocol,高級消息隊列協議,是應用層協議的一個開放標準,為面向消息的中間件設計。消息中間件主要用于組件之間的解耦,消息的發送者無需知道消息使用者的存在,反之亦然。? 它可以使對應的客戶端(client)與對應的消息中間件(broker)進行交互。消息中間件從發布者(publisher)那里收到消息(發布消息的應用,也稱為producer),然后將他們轉發給消費者(consumers,處理消息的應用)。由于AMQP是一個網絡協議,所以發布者、消費者以及消息中間件可以部署到不同的物理機器上面。
2.3 RabbitMQ 之Exchange類型
Exchange分發消息時根據類型的不同分發策略有區別,目前共四種類型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由鍵,此外 headers 交換器和 direct 交換器完全一致,但性能差很多,目前幾乎用不到了,所以直接看另外三種類型:
(1)Direct:消息中的路由鍵(routing key)如果和 Binding 中的 binding key 一致, 交換器就將消息發到對應的隊列中。路由鍵與隊列名完全匹配,如果一個隊列綁定到交換機要求路由鍵為“dog”,則只轉發 routing key 標記為“dog”的消息,不會轉發“dog.puppy”,也不會轉發“dog.guard”等等。它是完全匹配、單播的模式。
(2)fanout:每個發到fanout類型交換器的消息都會分到所有綁定的隊列上去。fanout 交換器不處理路由鍵,只是簡單的將隊列綁定到交換器上,每個發送到交換器的消息都會被轉發到與該交換器綁定的所有隊列上。很像子網廣播,每臺子網內的主機都獲得了一份復制的消息。fanout 類型轉發消息是最快的。
(3)topic:topic交換器通過模式匹配分配消息的路由鍵屬性,將路由鍵和某個模式進行匹配,此時隊列需要綁定到一個模式上。它將路由鍵和綁定鍵的字符串切分成單詞,這些單詞之間用點隔開。它同樣也會識別兩個通配符:符號“#”和符號“”。#匹配0個或多個單詞,匹配不多不少一個單詞。
(4)headers:headers類型的Exchange不依賴于routing key與binding key的匹配規則來路由消息,而是根據發送的消息內容中的headers屬性進行匹配。
在綁定Queue與Exchange時指定一組鍵值對;當消息發送到Exchange時,RabbitMQ會取到該消息的headers(也是一個鍵值對的形式),對比其中的鍵值對是否完全匹配Queue與Exchange綁定時指定的鍵值對;如果完全匹配則消息會路由到該Queue,否則不會路由到該Queue。
2.4 RabbitMQ的名詞
Broker:簡單來說就是消息隊列服務器實體。
Exchange:消息交換機,它指定消息按什么規則,路由到哪個隊列。
Queue:消息隊列載體,每個消息都會被投入到一個或多個隊列。
Binding:綁定,它的作用就是把exchange和queue按照路由規則綁定起來。
Routing Key:路由關鍵字,exchange根據這個關鍵字進行消息投遞。
vhost:虛擬主機,一個broker里可以開設多個vhost,用作不同用戶的權限分離。
Producer:消息生產者,就是投遞消息的程序。
Consumer:消息消費者,就是接受消息的程序。
Channel:消息通道,在客戶端的每個連接里,可建立多個channel,每個channel代表一個會話任務。
2.5 RabbitMQ消息隊列的使用過程
1、客戶端連接到消息隊列服務器,打開一個channel。
2、客戶端聲明一個exchange,并設置相關屬性。
3、客戶端聲明一個queue,并設置相關屬性。
4、客戶端使用routing key,在exchange和queue之間建立好綁定關系。
5、客戶端投遞消息到exchange。
6、exchange接收到消息后,就根據消息的key和已經設由binding,進行消息路里,將消息投遞到一個或多個隊列里
ps:通過durable參數來進行exchang、queue、消息持久化
三、RabbitMQ集群
RabbitMQ最優秀的功能之一就是內建集群,這個功能設計的目的是允許消費者和生產者在節點崩潰的情況下繼續運行,以及通過添加更多的節點來線性擴展消息通信吞吐量。RabbitMQ 內部利用 Erlang 提供的分布式通信框架 OTP 來滿足上述需求,使客戶端在失去一個 RabbitMQ 節點連接的情況下,還是能夠重新連接到集群中的任何其他節點繼續生產、消費消息。
3.1 RabbitMQ集群中的一些概念
RabbitMQ會始終記錄以下四種類型的內部元數據:
(1)隊列元數據包括隊列名稱和它們的屬性,比如是否可持久化,是否自動刪除
(2)交換器元數據交換器名稱、類型、屬性
(3)綁定元數據內部是一張表格記錄如何將消息路由到隊列
(4)vhost元數據為 vhost 內部的隊列、交換器、綁定提供命名空間和安全屬性
在單一節點中,RabbitMQ會將所有這些信息存儲在內存中,同時將標記為可持久化的隊列、交換器、綁定存儲到硬盤上。存到硬盤上可以確保隊列和交換器在節點重啟后能夠重建。而在集群模式下同樣也提供兩種選擇:存到硬盤上(獨立節點的默認設置),存在內存中。
如果在集群中創建隊列,集群只會在單個節點而不是所有節點上創建完整的隊列信息(元數據、狀態、內容)。結果是只有隊列的所有者節點知道有關隊列的所有信息,因此當集群節點崩潰時,該節點的隊列和綁定就消失了,并且任何匹配該隊列的綁定的新消息也丟失了。還好RabbitMQ 2.6.0之后提供了鏡像隊列以避免集群節點故障導致的隊列內容不可用。
RabbitMQ集群中可以共享 user、vhost、exchange等,所有的數據和狀態都是必須在所有節點上復制的,例外就是上面所說的消息隊列。RabbitMQ 節點可以動態的加入到集群中。
當在集群中聲明隊列、交換器、綁定的時候,這些操作會直到所有集群節點都成功提交元數據變更后才返回。集群中有內存節點和磁盤節點兩種類型,內存節點雖然不寫入磁盤,但是它的執行比磁盤節點要好。內存節點可以提供出色的性能,磁盤節點能保障配置信息在節點重啟后仍然可用,那集群中如何平衡這兩者呢?
RabbitMQ只要求集群中至少有一個磁盤節點,所有其他節點可以是內存節點,當節點加入或離開集群時,它們必須要將該變更通知到至少一個磁盤節點。如果只有一個磁盤節點,剛好又是該節點崩潰了,那么集群可以繼續路由消息,但不能創建隊列、創建交換器、創建綁定、添加用戶、更改權限、添加或刪除集群節點。換句話說集群中的唯一磁盤節點崩潰的話,集群仍然可以運行,但直到該節點恢復,否則無法更改任何東西。
3.2 RabbitMQ集群配置和啟動
如果是在一臺機器上同時啟動多個RabbitMQ節點來組建集群的話,只用上面介紹的方式啟動第二、第三個節點將會因為節點名稱和端口沖突導致啟動失敗。所以在每次調用 rabbitmq-server 命令前,設置環境變量 RABBITMQ_NODENAME 和 RABBITMQ_NODE_PORT 來明確指定唯一的節點名稱和端口。下面的例子端口號從5672開始,每個新啟動的節點都加1,節點也分別命名為test_rabbit_1、test_rabbit_2、test_rabbit_3。
啟動第1個節點:
RABBITMQ_NODENAME=test_rabbit_1 RABBITMQ_NODE_PORT=5672?./sbin/rabbitmq-server -detached
啟動第2個節點:
RABBITMQ_NODENAME=test_rabbit_2 RABBITMQ_NODE_PORT=5673?./sbin/rabbitmq-server -detached
啟動第2個節點前建議將 RabbitMQ 默認激活的插件關掉,否則會存在使用了某個插件的端口號沖突,導致節點啟動不成功。
現在第2個節點和第1個節點都是獨立節點,它們并不知道其他節點的存在。集群中除第一個節點外后加入的節點需要獲取集群中的元數據,所以要先停止 Erlang 節點上運行的 RabbitMQ 應用程序,并重置該節點元數據,再加入并且獲取集群的元數據,最后重新啟動 RabbitMQ 應用程序。
停止第2個節點的應用程序:
./sbin/rabbitmqctl -n test_rabbit_2 stop_app
重置第2個節點元數據:
./sbin/rabbitmqctl -n test_rabbit_2 reset
第2節點加入第1個節點組成的集群:
./sbin/rabbitmqctl -n test_rabbit_2 join_cluster test_rabbit_1@localhost
啟動第2個節點的應用程序
./sbin/rabbitmqctl -n test_rabbit_2 start_app
第3個節點的配置過程和第2個節點類似:
RABBITMQ_NODENAME=test_rabbit_3 RABBITMQ_NODE_PORT=5674 ./sbin/rabbitmq-server -detached./sbin/rabbitmqctl -n test_rabbit_3 stop_app./sbin/rabbitmqctl -n test_rabbit_3 reset./sbin/rabbitmqctl -n test_rabbit_3 join_cluster test_rabbit_1@localhost./sbin/rabbitmqctl -n test_rabbit_3 start_app
3.3 RabbitMQ集群運維
停止某個指定的節點,比如停止第2個節點:
RABBITMQ_NODENAME=test_rabbit_2 ./sbin/rabbitmqctl stop
查看節點3的集群狀態:
./sbin/rabbitmqctl -n test_rabbit_3 cluster_status
3.4 RabbitMQ集群元數據的同步
RabbitMQ集群會始終同步四種類型的內部元數據(類似索引): a.隊列元數據:隊列名稱和它的屬性; b.交換器元數據:交換器名稱、類型和屬性; c.綁定元數據:一張簡單的表格展示了如何將消息路由到隊列; d.vhost元數據:為vhost內的隊列、交換器和綁定提供命名空間和安全屬性; 因此,當用戶訪問其中任何一個RabbitMQ節點時,通過rabbitmqctl查詢到的queue/user/exchange/vhost等信息都是相同的。
3.5 RabbitMQ集群僅采用元數據同步的方式
我想肯定有不少同學會問,想要實現HA方案,那將RabbitMQ集群中的所有Queue的完整數據在所有節點上都保存一份不就可以了么?(可以類似MySQL的主主模式嘛)這樣子,任何一個節點出現故障或者宕機不可用時,那么使用者的客戶端只要能連接至其他節點能夠照常完成消息的發布和訂閱嘛。 我想RabbitMQ的作者這么設計主要還是基于集群本身的性能和存儲空間上來考慮。第一,存儲空間,如果每個集群節點都擁有所有Queue的完全數據拷貝,那么每個節點的存儲空間會非常大,集群的消息積壓能力會非常弱(無法通過集群節點的擴容提高消息積壓能力);第二,性能,消息的發布者需要將消息復制到每一個集群節點,對于持久化消息,網絡和磁盤同步復制的開銷都會明顯增加。
3.6 RabbitMQ集群發送/訂閱消息的基本原理
場景1、客戶端直接連接隊列所在節點
如果有一個消息生產者或者消息消費者通過amqp-client的客戶端連接至節點1進行消息的發布或者訂閱,那么此時的集群中的消息收發只與節點1相關,這個沒有任何問題;如果客戶端相連的是節點2或者節點3(隊列1數據不在該節點上),那么情況又會是怎么樣呢?
場景2、客戶端連接的是非隊列數據所在節點
如果消息生產者所連接的是節點2或者節點3,此時隊列1的完整數據不在該兩個節點上,那么在發送消息過程中這兩個節點主要起了一個路由轉發作用,根據這兩個節點上的元數據(也就是上文提到的:指向queue的owner node的指針)轉發至節點1上,最終發送的消息還是會存儲至節點1的隊列1上。 同樣,如果消息消費者所連接的節點2或者節點3,那這兩個節點也會作為路由節點起到轉發作用,將會從節點1的隊列1中拉取消息進行消費。
四、RabbitMQ的幾種工作模式
4.1 Work模式?
一個生產者,多個消費者,每個消費者獲取到的消息唯一。
1、 ?自動模式
消費者從消息隊列獲取消息后,服務端就認為該消息已經成功消費。
2、 ?手動模式
消費者從消息隊列獲取消息后,服務端并沒有標記為成功消費
消費者成功消費后需要將狀態返回到服務端
4.2 PS訂閱模式
一個生產者發送的消息會被多個消費者獲取。
生產者:可以將消息發送到隊列或者是交換機。
消費者:只能從隊列中獲取消息。
如果消息發送到沒有隊列綁定的交換機上,那么消息將丟失。
4.3 Routing路由模式
1、 發送消息到交換機并且要指定路由key
2、 消費者將隊列綁定到交換機時需要指定路由key
4.4 Topics?主題模式
將路由鍵和某模式進行匹配,此時隊列需要綁定在一個模式上,“#”匹配一個詞或多個詞,“*”只匹配一個詞。
4.5 RPC模式?
(RPC) Remote Procedure Call Protocol遠程過程調用協議
在一個大型的公司,系統由大大小小的服務構成,不同的團隊維護不同的代碼,部署在不同的機器。但是在做開發時候往往要用到其它團隊的方法,因為已經有了實現。但是這些服務部署不同的機器上,想要調用就需要網絡通信,這些代碼繁瑣且復雜,一不小心就會寫的很低效。RPC協議定義了規劃,其它的公司都給出了不同的實現。比如微軟的wcf,以及現在火熱的WebApi。
在RabbitMQ中RPC的實現也是很簡單高效的,現在我們的客戶端、服務端都是消息發布者與消息接收者。
首先客戶端通過RPC向服務端發出請求
我這里有一堆東西需要你給我處理一下,correlation_id:這是我的請求標識,erply_to:你處理完過后把結果返回到這個隊列中。
服務端拿到了請求,開始處理并返回
correlation_id:這是你的請求標識 ,原封不動的給你。 這時候客戶端用自己的correlation_id與服務端返回的id進行對比。是我的,就接收。