應用場景1-“Hello Word”
一個P向queue發送一個message,一個C從該queue接收message並打印。
producer,連接至RabbitMQ Server,聲明隊列,發送message,關閉連接,退出。
應用場景2-work queues
工作隊列(又稱:任務隊列——Task Queues)是爲了避免等待一些佔用大量資源、時間的操作。當我們把任務(Task)當作消息發送到隊列中,一個運行在後臺的工作者(worker)進程就會取出任務然後處理。當你運行多個工作者(workers),任務就會在它們之間共享。
循環調度
使用工作隊列的一個好處就是它能夠並行的處理隊列。如果堆積了很多任務,我們只需要添加更多的工作者(workers)就可以了,擴展很簡單。
默認來說,RabbitMQ 會按順序得把消息發送給每個消費者(consumer)。平均每個消費者都會收到同等數量得消息。這種發送消息得方式叫做——輪詢(round-robin)。
消息確認
當處理一個比較耗時得任務的時候,你也許想知道消費者(consumers)是否運行到一半就掛掉。當消息被 RabbitMQ 發送給消費者(consumers)之後,馬上就會在內存中移除。這種情況,你只要把一個工作者(worker)停止,正在處理的消息就會丟失。同時,所有發送到這個工作者的還沒有處理的消息都會丟失。
我們不想丟失任何任務消息。如果一個工作者(worker)掛掉了,我們希望任務會重新發送給其他的工作者(worker)。
爲了防止消息丟失,RabbitMQ 提供了消息響應(acknowledgments)。消費者會通過一個 ack(響應),告訴 RabbitMQ 已經收到並處理了某條消息,然後RabbitMQ 就會釋放並刪除這條消息。
如果消費者(consumer)掛掉了,沒有發送響應,RabbitMQ 就會認爲消息沒有被完全處理,然後重新發送給其他消費者(consumer)。這樣,即使工作者(workers)偶爾的掛掉,也不會丟失消息。
消息是沒有超時這個概念的;當工作者與它斷開連的時候,RabbitMQ 會重新發送消息。這樣在處理一個耗時非常長的消息任務的時候就不會出問題了。
消息響應默認是開啓的。可以使用 no_ack=True 標識把它關閉。當工作者(worker)完成了任務,就發送一個響應。
忘記確認
一個很容易犯的錯誤就是忘了 basic_ack,後果很嚴重。消息在你的程序退出之後就會重新發送,如果它不能夠釋放沒響應的消息,RabbitMQ 就會佔用越來越多的內存。
消息持久化
如果你沒有特意告訴 RabbitMQ,那麼在它退出或者崩潰的時候,將會丟失所有隊列和消息。爲了確保信息不會丟失,有兩個事情是需要注意的:我們必須把“隊列”和“消息”設爲持久化。
注意:消息持久化
將消息設爲持久化並不能完全保證不會丟失。 RabbitMq 要把消息存到硬盤,但從 RabbitMq 收到消息到保存之間還是有一個很小的間隔時間。因爲 RabbitMq 並不是所有的消息都使用 fsync(2) ——它有可能只是保存到緩存中,並不一定會寫到硬盤中。並不能保證真正的持久化,但已經足夠應付我們的簡單工作隊列。如果你一定要保證持久化,你需要改寫你的代碼來支持事務(transaction)。
公平調度
你應該已經發現,它仍舊沒有按照我們期望的那樣進行分發。比如有兩個工作者(workers),處理奇數消息的比較繁忙,處理偶數消息的比較輕鬆。然而RabbitMQ 並不知道這些,它仍然一如既往的派發消息。
這時因爲 RabbitMQ 只管分發進入隊列的消息,不會關心有多少消費者(consumer)沒有作出響應。它盲目的把第 n-th 條消息發給第 n-th 個消費者。
我們可以使用 basic.qos 方法,並設置 prefetch_count=1。這樣是告訴RabbitMQ,再同一時刻,不要發送超過1條消息給一個工作者(worker),直到它已經處理了上一條消息並且作出了響應。這樣,RabbitMQ 就會把消息分發給下一個空閒的工作者(worker)。
關於隊列大小
如果所有的工作者都處理繁忙狀態,你的隊列就會被填滿。你需要留意這個問題,要麼添加更多的工作者(workers),要麼使用其他策略。
應用場景3-Publish/Subscribe
爲了描述這種模式,我們將會構建一個簡單的日誌系統。它包括兩個程序——第一個程序負責發送日誌消息,第二個程序負責獲取消息並輸出內容。
在我們的這個日誌系統中,所有正在運行的接收方程序都會接受消息。我們用其中一個接收者(receiver)把日誌寫入硬盤中,另外一個接受者(receiver)把日誌輸出到屏幕上。
最終,日誌消息被廣播給所有的接受者(receivers)。
交換機(Exchanges)
我們發送消息到隊列並從中取出消息。介紹 RabbitMQ 中完整的消息模型。
讓我們簡單的概括一下之前的教程:
發佈者(producer)是發佈消息的應用程序。
隊列(queue)用於消息存儲的緩衝。
消費者(consumer)是接收消息的應用程序。
RabbitMQ 消息模型的核心理念是:發佈者(producer)不會直接發送任何消息給隊列。事實上,發佈者(producer)甚至不知道消息是否已經被投遞到隊列。
發佈者(producer)只需要把消息發送給一個交換機(exchange)。交換機非常簡單,它一邊從發佈者方接收消息,一邊把消息推送到隊列。交換機必須知道如何處理它接收到的消息,是應該推送到指定的隊列還是是多個隊列,或者是直接忽略消息。這些規則是通過交換機類型(exchange type)來定義的。
有幾個可供選擇的交換機類型:直連交換機(direct), 主題交換機(topic), (頭交換機)headers和 扇型交換機(fanout)。我們在這裏主要說明最後一個 —— 扇型交換機(fanout)
扇型交換機(fanout)很簡單,你可能從名字上就能猜測出來,它把消息發送給它所知道的所有隊列。交換器列表
這個列表中有一些叫做 amq.* 的交換器。這些都是默認創建的,不過這時候你還不需要使用他們。
匿名的交換器
我們對交換機一無所知,但仍然能夠發送消息到隊列中。因爲我們使用了命名爲空字符串 ("") 默認的交換機。
exchange 參數就是交換機的名稱。空字符串代表默認或者匿名交換機:消息將會根據指定的 routing_key 分發到指定的隊列。
臨時隊列
給一個隊列命名是很重要的——我們需要把工作者(workers)指向正確的隊列。如果你打算在發佈者(producers)和消費者(consumers)之間共享同隊列的話,給隊列命名是十分重要的。
首先,當我們連接上 RabbitMQ 的時候,我們需要一個全新的、空的隊列。我們可以手動創建一個隨機的隊列名,或者讓服務器爲我們選擇一個隨機的隊列名(推薦)。
第二步,當與消費者(consumer)斷開連接的時候,這個隊列應當被立即刪除。exclusive 標識符即可達到此目的。
綁定(Bindings)
我們已經創建了一個扇型交換機(fanout)和一個隊列。現在我們需要告訴交換機如何發送消息給我們的隊列。交換器和隊列之間的聯繫我們稱之爲綁定(binding)。
綁定(binding)列表
你可以使用 rabbitmqctl list_bindings 列出所有現存的綁定。
最重要的改變就是我們把消息發送給 logs 交換機而不是匿名交換機。在發送的時候我們需要提供 routing_key參數,但是它的值會被扇型交換機(fanout exchange)忽略。
應用場景4-Routing
綁定(Bindings)
綁定(binding)是指交換機(exchange)和隊列(queue)的關係。可以簡單理解爲:這個隊列(queue)對這個交換機(exchange)的消息感興趣。
綁定的時候可以帶上一個額外的 routing_key 參數。爲了避免與basic_publish的參數混淆,我們把它叫做綁定鍵(binding key)。
綁定鍵的意義取決於交換機(exchange)的類型。我們之前使用過的扇型交換機(fanout exchanges)會忽略這個值。
直連交換機(Direct exchange)
我們也許只是希望將比較嚴重的錯誤(error)日誌寫入磁盤,以免在警告(warning)或者信息(info)日誌上浪費磁盤空間。
我們使用的扇型交換機(fanout exchange)沒有足夠的靈活性 —— 它能做的僅僅是廣播。
我們將會使用直連交換機(direct exchange)來代替。路由的算法很簡單 —— 交換機將會對綁定鍵(binding key)和路由鍵(routing key)進行精確匹配,從而確定消息該分發到哪個隊列。
下圖能夠很好的描述這個場景:
在這個場景中,我們可以看到直連交換機 X 和兩個隊列進行了綁定。第一個隊列使用 orange 作爲綁定鍵,第二個隊列有兩個綁定,一個使用 black 作爲綁定鍵,另外一個使用 green。
這樣以來,當路由鍵爲 orange 的消息發佈到交換機,就會被路由到隊列 Q1。路由鍵爲 black 或者 green 的消息就會路由到 Q2。其他的所有消息都將會被丟棄。
多個綁定(Multiple bindings)
多個隊列使用相同的綁定鍵是合法的。這個例子中,我們可以添加一個 X 和 Q1 之間的綁定,使用 black 綁定鍵。這樣一來,直連交換機就和扇型交換機的行爲一樣,會將消息廣播到所有匹配的隊列。帶有 black 路由鍵的消息會同時發送到 Q1 和 Q2。
發送日誌
我們將會發送消息到一個直連交換機,把日誌級別作爲路由鍵。這樣接收日誌的腳本就可以根據嚴重級別來選擇它想要處理的日誌。
訂閱
處理接收消息的方式和之前差不多,只有一個例外,我們將會爲我們感興趣的每個嚴重級別分別創建一個新的綁定。
應用場景5-topic
主題交換機
發送到主題交換機(topic exchange)的消息不可以攜帶隨意什麼樣子的路由鍵(routing_key),它的路由鍵必須是一個由.分隔開的詞語列表。這些單詞隨便是什麼都可以,但是最好是跟攜帶它們的消息有關係的詞彙。以下是幾個推薦的例子:"stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit"。詞語的個數可以隨意,但是不要超過 255 字節。
綁定鍵也必須擁有同樣的格式。主題交換機背後的邏輯跟直連交換機很相似 —— 一個攜帶着特定路由鍵的消息會被主題交換機投遞給綁定鍵與之想匹配的隊列。但是它的綁定鍵和路由鍵有兩個特殊應用方式:
(星號) 用來表示一個單詞.
(井號) 用來表示任意數量(零個或多個)單詞。
下邊用圖說明:
這個例子裏,我們發送的所有消息都是用來描述小動物的。發送的消息所攜帶的路由鍵是由三個單詞所組成的,這三個單詞被兩個.分割開。路由鍵裏的第一個單詞描述的是動物的手腳的利索程度,第二個單詞是動物的顏色,第三個是動物的種類。所以它看起來是這樣的: ..。
我們創建了三個綁定:Q1 的綁定鍵爲 .orange.,Q2 的綁定鍵爲 ..rabbit 和 lazy.# 。
這三個綁定鍵被可以總結爲:
Q1 對所有的桔×××動物都感興趣。
Q2 則是對所有的兔子和所有懶惰的動物感興趣。
一個攜帶有 quick.orange.rabbit 的消息將會被分別投遞給這兩個隊列。攜帶着 lazy.orange.elephant 的消息同樣也會給兩個隊列都投遞過去。另一方面攜帶有 quick.orange.fox 的消息會投遞給第一個隊列,攜帶有 lazy.brown.fox 的消息會投遞給第二個隊列。攜帶有 lazy.pink.rabbit 的消息只會被投遞給第二個隊列一次,即使它同時匹配第二個隊列的兩個綁定。攜帶着 quick.brown.fox 的消息不會投遞給任何一個隊列。
如果我們違反約定,發送了一個攜帶有一個單詞或者四個單詞("orange" or "quick.orange.male.rabbit")的消息時,發送的消息不會投遞給任何一個隊列,而且會丟失掉。
但是另一方面,即使 "lazy.orange.male.rabbit" 有四個單詞,他還是會匹配最後一個綁定,並且被投遞到第二個隊列中。
主題交換機
主題交換機是很強大的,它可以表現出跟其他交換機類似的行爲
當一個隊列的綁定鍵爲 "#"(井號) 的時候,這個隊列將會無視消息的路由鍵,接收所有的消息。
當 * (星號) 和 # (井號) 這兩個特殊字符都未在綁定鍵中出現的時候,此時主題交換機就擁有的直連交換機的行爲。
應用場景6-PRC
關於 RPC 的注意事項
儘管 RPC 在計算領域是一個常用模式,但它也經常被詬病。當一個問題被拋出的時候,程序員往往意識不到這到底是由本地調用還是由較慢的 RPC 調用引起的。同樣的困惑還來自於系統的不可預測性和給調試工作帶來的不必要的複雜性。跟軟件精簡不同的是,濫用 RPC 會導致不可維護的麪條代碼.
考慮到這一點,牢記以下建議:
確保能夠明確的搞清楚哪個函數是本地調用的,哪個函數是遠程調用的。給你的系統編寫文檔。保持各個組件間的依賴明確。處理錯誤案例。明確客戶端該如何處理 RPC 服務器的宕機和長時間無響應情況。
當對避免使用 RPC 有疑問的時候。如果可以的話,你應該儘量使用異步管道來代替RPC 類的阻塞。結果被異步地推送到下一個計算場景。
回調隊列
一般來說通過 RabbitMQ 來實現 RPC 是很容易的。一個客戶端發送請求信息,服務器端將其應用到一個回覆信息中。爲了接收到回覆信息,客戶端需要在發送請求的時候同時發送一個回調隊列(callback queue)的地址。
消息屬性
AMQP 協議給消息預定義了一系列的14個屬性。大多數屬性很少會用到,除了以下幾個:
delivery_mode(投遞模式):將消息標記爲持久的(值爲2)或暫存的(除了2之外的其他任何值)。
content_type(內容類型):用來描述編碼的 mime-type。例如在實際使用中常常使用 application/json 來描述 JOSN 編碼類型。
reply_to(回覆目標):通常用來命名回調隊列。
correlation_id(關聯標識):用來將RPC的響應和請求關聯起來。
關聯標識
上邊介紹的方法中,我們建議給每一個 RPC 請求新建一個回調隊列。這不是一個高效的做法,幸好這兒有一個更好的辦法 —— 我們可以爲每個客戶端只建立一個獨立的回調隊列。
這就帶來一個新問題,當此隊列接收到一個響應的時候它無法辨別出這個響應是屬於哪個請求的。correlation_id 就是爲了解決這個問題而來的。我們給每個請求設置一個獨一無二的值。當我們從回調隊列中接收到一個消息的時候,我們就可以查看這條屬性從而將響應和請求匹配起來。如果我們接手到的消息的correlation_id 是未知的,那就直接銷燬掉它,因爲它不屬於我們的任何一條請求。
你也許會問,爲什麼我們接收到未知消息的時候不拋出一個錯誤,而是要將它忽略掉?這是爲了解決服務器端有可能發生的競爭情況。儘管可能性不大,但RPC服務器還是有可能在已將應答發送給我們但還未將確認消息發送給請求的情況下死掉。如果這種情況發生,RPC 在重啓後會重新處理請求。這就是爲什麼我們必須在客戶端優雅的處理重複響應,同時RPC也需要儘可能保持冪等性。
總結
我們的 RPC 如此工作:
當客戶端啓動的時候,它創建一個匿名獨享的回調隊列。
在 RPC 請求中,客戶端發送帶有兩個屬性的消息:一個是設置回調隊列的 reply_to 屬性,另一個是設置唯一值的 correlation_id 屬性。
將請求發送到一個 rpc_queue 隊列中。
RPC 工作者(又名:服務器)等待請求發送到這個隊列中來。當請求出現的時候,它執行他的工作並且將帶有執行結果的消息發送給 reply_to 字段指定的隊列。
客戶端等待回調隊列裏的數據。當有消息出現的時候,它會檢查 correlation_id 屬性。如果此屬性的值與請求匹配,將它返回給應用。 整合到一起