Rabbitmq學習筆記

什么叫消息隊列

????????消息(Message)是指在應用間傳送的數據。消息可以非常簡單,比如只包含文本字符串,也可以更復雜,可能包含嵌入對象。

????????消息隊列(Message Queue)是一種應用間的通信方式,消息發送后可以立即返回,由消息系統來確保消息的可靠傳遞。消息發布者只管把消息發布到 MQ 中而不用管誰來取,消息使用者只管從 MQ 中取消息而不管是誰發布的。這樣發布者和使用者都不用知道對方的存在。


為何用消息隊列

????????從上面的描述中可以看出消息隊列是一種應用間的異步協作機制,那什么時候需要使用 MQ 呢?

????????以常見的訂單系統為例,用戶點擊【下單】按鈕之后的業務邏輯可能包括:扣減庫存、生成相應單據、發紅包、發短信通知。在業務發展初期這些邏輯可能放在一起同步執行,隨著業務的發展訂單量增長,需要提升系統服務的性能,這時可以將一些不需要立即生效的操作拆分出來異步執行,比如發放紅包、發短信通知等。這種場景下就可以用 MQ ,在下單的主流程(比如扣減庫存、生成相應單據)完成之后發送一條消息到 MQ 讓主流程快速完結,而由另外的單獨線程拉取MQ的消息(或者由 MQ 推送消息),當發現 MQ 中有發紅包或發短信之類的消息時,執行相應的業務邏輯。

????????以上是用于業務解耦的情況,其它常見場景包括最終一致性、廣播、錯峰流控等等。


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 提供了許多插件,來從多方面進行擴展,也可以編寫自己的插件。


RabbitMQ 中的概念模型

消息模型

????????所有 MQ 產品從模型抽象上來說都是一樣的過程:消費者(consumer)訂閱某個隊列。生產者(producer)創建消息,然后發布到隊列(queue)中,最后將消息發送到監聽的消費者。


RabbitMQ 基本概念

????????上面只是最簡單抽象的描述,具體到 RabbitMQ 則有更詳細的概念需要解釋。上面介紹過 RabbitMQ 是 AMQP 協議的一個開源實現,所以其內部實際上也是 AMQP 中的基本概念:

(1)Message:消息,消息是不具名的,它由消息頭和消息體組成。消息體是不透明的,而消息頭則由一系列的可選屬性組成,這些屬性包括routing-key(路由鍵)、priority(相對于其他消息的優先權)、delivery-mode(指出該消息可能需要持久性存儲)等。

(2)Publisher:消息的生產者,也是一個向交換器發布消息的客戶端應用程序

(3)Exchange:交換器,用來接收生產者發送的消息并將這些消息路由給服務器中的隊列

(4)Binding:綁定,用于消息隊列和交換器之間的關聯。一個綁定就是基于路由鍵將交換器和消息隊列連接起來的路由規則,所以可以將交換器理解成一個由綁定構成的路由表

(5)Queue:消息隊列,用來保存消息直到發送給消費者。它是消息的容器,也是消息的終點。一個消息可投入一個或多個隊列。消息一直在隊列里面,等待消費者連接到這個隊列將其取走

(6)Connection:網絡連接,比如一個TCP連接

(7)Channel:信道,多路復用連接中的一條獨立的雙向數據流通道。信道是建立在真實的TCP連接內地虛擬連接,AMQP 命令都是通過信道發出去的,不管是發布消息、訂閱隊列還是接收消息,這些動作都是通過信道完成。因為對于操作系統來說建立和銷毀 TCP 都是非常昂貴的開銷,所以引入了信道的概念,以復用一條 TCP 連接。

(8)Consumer:消息的消費者,表示一個從消息隊列中取得消息的客戶端應用程序。

(9)Virtual Host:虛擬主機,表示一批交換器、消息隊列和相關對象。虛擬主機是共享相同的身份認證和加密環境的獨立服務器域。每個 vhost 本質上就是一個 mini 版的 RabbitMQ 服務器,擁有自己的隊列、交換器、綁定和權限機制。vhost 是 AMQP 概念的基礎,必須在連接時指定,RabbitMQ 默認的 vhost 是/ (每個virtual host本質上都是一個RabbitMQ Server,擁有它自己的queue,exchagne,和bings rule等等。這保證了你可以在多個不同的application中使用RabbitMQ。)

(10)Broker:表示消息隊列服務器實體。


AMQP 中的消息路由

????????AMQP 中消息的路由過程和 Java 開發者熟悉的 JMS 存在一些差別,AMQP 中增加了 Exchange 和 Binding 的角色。生產者把消息發布到 Exchange 上,消息最終到達隊列并被消費者接收,而 Binding 決定交換器的消息應該發送到那個隊列。


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”等等。它是完全匹配、單播的模式。

????????如果 routing key 匹配, 那么Message就會被傳遞到相應的queue中。其實在queue創建時,它會自動的以queue的名字作為routing key來綁定那個exchange

????????Direct exchange的路由算法非常簡單:通過binding key的完全匹配,可以通過下圖來說明


????????exchange X和兩個queue綁定在一起。Q1的binding key是orange。Q2的binding key是black和green。當P publish key是orange時,exchange會把它放到Q1。如果是black或者green那么就會到Q2。其余的Message都會被丟棄。

(2)fanout

????????每個發到 fanout 類型交換器的消息都會分到所有綁定的隊列上去。fanout 交換器不處理路由鍵,只是簡單的將隊列綁定到交換器上,每個發送到交換器的消息都會被轉發到與該交換器綁定的所有隊列上。很像子網廣播,每臺子網內的主機都獲得了一份復制的消息。fanout 類型轉發消息是最快的。

(3)topic

????????topic 交換器通過模式匹配分配消息的路由鍵屬性,將路由鍵和某個模式進行匹配,此時隊列需要綁定到一個模式上。它將路由鍵和綁定鍵的字符串切分成單詞,這些單詞之間用點隔開。它同樣也會識別兩個通配符:符號“#”和符號“”。#匹配0個或多個單詞,匹配不多不少一個單詞。


基本示例

發送端 producer

import ?pika

# 建立一個實例

connection = pika.BlockingConnection(

????????pika.ConnectionParameters('localhost',5672)# 默認端口5672,可不寫)

?# 聲明一個管道,在管道里發消息

channel = connection.channel()

# 在管道里聲明queue

channel.queue_declare(queue='hello’)

# RabbitMQ a message can never be sent directly to the queue, it always needs to go through an exchange.

channel.basic_publish(exchange='',

????????????????????????????????????????????routing_key='hello',# queue名字

????????????????????????????????????????????body='Hello World!')# 消息內容

print(" [x] Sent 'Hello World!'")

connection.close(). # 隊列關閉


接收端 consumer

import pika

import time

# 建立實例

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost’))

# 聲明管道

channel = connection.channel()

# 為什么又聲明了一個‘hello’隊列?# 如果確定已經聲明了,可以不聲明。但是你不知道那個機器先運行,所以要聲明兩次。

channel.queue_declare(queue='hello’)

def ?callback(ch, method, properties, body):

# 四個參數為標準格式

? ??????print(ch, method, properties)# 打印看一下是什么# 管道內存對象 內容相關信息 后面講

? ??????print(" [x] Received %r"% body)

????????time.sleep(15)

????????ch.basic_ack(delivery_tag = method.delivery_tag)# 告訴生成者,消息處理完成

channel.basic_consume(# 消費消息

????????????callback,# 如果收到消息,就調用callback函數來處理消息

????????????queue='hello',# 你要從那個隊列里收消息#?

? ? ? ? ? ? #no_ack=True? # 寫的話,如果接收消息,機器宕機消息就丟了# 一般不寫。宕機則生產者檢測到發給其他消費者)

print(' [*] Waiting for messages. To exit press CTRL+C')

channel.start_consuming()# 開始消費消息


RabbitMQ 消息分發輪詢

(1)上面的只是一個生產者、一個消費者,能不能一個生產者多個消費者呢?可以上面的例子,多啟動幾個消費者consumer,看一下消息的接收情況。采用輪詢機制;把消息依次分發

(2)假如消費者處理消息需要15秒,如果當機了,那這個消息處理明顯還沒處理完,怎么處理?(可以模擬消費端斷了,分別注釋和不注釋 no_ack=True 看一下)你沒給我回復確認,就代表消息沒處理完。

(3)上面的效果消費端斷了就轉到另外一個消費端去了,但是生產者怎么知道消費端斷了呢?因為生產者和消費者是通過socket連接的,socket斷了,就說明消費端斷開了。

(4)上面的模式只是依次分發,實際情況是機器配置不一樣。怎么設置類似權重的操作?RabbitMQ怎么辦呢,RabbitMQ做了簡單的處理就能實現公平的分發。就是RabbitMQ給消費者發消息的時候檢測下消費者里的消息數量,如果超過指定值(比如1條),就不給你發了。


只需要在消費者端,channel.basic_consume前加上就可以了。

????????channel.basic_qos(prefetch_count=1)# 類似權重,按能力分發,如果有一個消息,就不在給你發 ? ? ? ? ? ? ? channel.basic_consume(# 消費消息


RabbitMQ 消息持久化(durable、properties)

rabbitmqctl list_queues# 查看當前queue數量及queue里消息數量

消息持久化

如果隊列里還有消息,RabbitMQ 服務端宕機了呢?消息還在不在?把RabbitMQ服務重啟,看一下消息在不在。上面的情況下,宕機了,消息就久了,下面看看如何把消息持久化。

每次聲明隊列的時候,都加上durable,注意每個隊列都得寫,客戶端、服務端聲明的時候都得寫。

# 在管道里聲明

queuechannel.queue_declare(queue='hello2', durable=True)


測試結果發現,只是把隊列持久化了,但是隊列里的消息沒了。durable的作用只是把隊列持久化。離消息持久話還差一步:發送端發送消息時,加上properties

properties=pika.BasicProperties(

delivery_mode=2,# 消息持久化)


發送端 producer

importpika

connection = pika.BlockingConnection(pika.ConnectionParameters(

????????????????????????????'localhost',5672))# 默認端口5672,可不寫

channel = connection.channel()

#聲明queue

channel.queue_declare(queue='hello2', durable=True)# 若聲明過,則換一個名字

#n RabbitMQ a message can never be sent directly to the queue, it always needs to go through an exchange.

channel.basic_publish(exchange='',

????????????routing_key='hello2',

????????????body='Hello World!',

????????????properties=pika.BasicProperties(

????????????????delivery_mode=2,# make message persistent)

)

print(" [x] Sent 'Hello World!'")

connection.close()


接收端 consumer

import pika

import time

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))

channel = connection.channel()

channel.queue_declare(queue='hello2', durable=True)

def ?callback(ch, method, properties, body):

????????print(" [x] Received %r"% body)

????????time.sleep(10)

????????ch.basic_ack(delivery_tag = method.delivery_tag)# 告訴生產者,消息處理完成

channel.basic_qos(prefetch_count=1)# 類似權重,按能力分發,如果有一個消息,就不在給你發channel.basic_consume(# 消費消息

????????????????????????callback,# 如果收到消息,就調用callback

????????????????????????queue='hello2’,

????????????????????????# no_ack=True? # 一般不寫,處理完接收處理結果。宕機則發給其他消費者)

print(' [*] Waiting for messages. To exit press CTRL+C')

channel.start_consuming()


RabbitMQ消息隊列(三):任務分發機制

????????當有Consumer需要大量的運算時,RabbitMQ Server需要一定的分發機制來balance每個Consumer的load。試想一下,對于web application來說,在一個很多的HTTP request里是沒有時間來處理復雜的運算的,只能通過后臺的一些工作線程來完成。接下來我們分布講解。

????????默認情況下,RabbitMQ 會順序的分發每個Message。當每個收到ack后,會將該Message刪除,然后將下一個Message分發到下一個Consumer。這種分發方式叫做round-robin。這種分發還有問題,接著向下讀吧。

????????每個Consumer可能需要一段時間才能處理完收到的數據。如果在這個過程中,Consumer出錯了,異常退出了,而數據還沒有處理完成,那么非常不幸,這段數據就丟失了。因為我們采用no-ack的方式進行確認,也就是說,每次Consumer接到數據后,而不管是否處理完成,RabbitMQ Server會立即把這個Message標記為完成,然后從queue中刪除了。

????????如果一個Consumer異常退出了,它處理的數據能夠被另外的Consumer處理,這樣數據在這種情況下就不會丟失了。為了保證數據不被丟失,RabbitMQ支持消息確認機制,即acknowledgments。為了保證數據能被正確處理而不僅僅是被Consumer收到,那么我們不能采用no-ack。而應該是在處理完數據后發送ack。(在處理數據后發送的ack,就是告訴RabbitMQ數據已經被接收,處理完成,RabbitMQ可以去安全的刪除它了,如果Consumer退出了但是沒有發送ack,那么RabbitMQ就會把這個Message發送到下一個Consumer。這樣就保證了在Consumer異常退出的情況下數據也不會丟失)


Message durability消息持久化

????????為了保證在RabbitMQ退出或者crash了數據仍沒有丟失,需要將queue和Message都要持久化。queue的持久化需要在聲明時指定durable=True:

????????????channel.queue_declare(queue='hello',?durable=True)

需要持久化Message,即在Publish的時候指定一個properties:

????????????channel.basic_publish(exchange='',

????????????????????????????????????????????????????routing_key="task_queue",

????????????????????????????????????????????????????body=message,

????????????????????????????????????????????????????properties=pika.BasicProperties(

????????????????????????????????????????????????????????????delivery_mode?=2,#?make?message?persistent

????????????????????????????????))

(RabbitMQ需要時間去把這些信息存到磁盤上,這個time window雖然短,但是它的確還是有。在這個時間窗口內如果數據沒有保存,數據還會丟失。還有另一個原因就是RabbitMQ并不是為每個Message都做fsync:它可能僅僅是把它保存到Cache里,還沒來得及保存到物理磁盤上。

因此這個持久化還是有問題。但是對于大多數應用來說,這已經足夠了。當然為了保持一致性,你可以把每次的publish放到一個transaction中。這個transaction的實現需要user defined codes)


Fair dispatch 公平分發

????????默認狀態下,RabbitMQ將第n個Message分發給第n個Consumer。當然n是取余后的。它不管Consumer是否還有unacked Message,只是按照這個默認機制進行分發。那么如果有個Consumer工作比較重,那么就會導致有的Consumer基本沒事可做,有的Consumer卻是毫無休息的機會。那么,RabbitMQ是如何處理這種問題呢?


????????通過basic.qos方法設置prefetch_count=1。這樣RabbitMQ就會使得每個Consumer在同一個時間點最多處理一個Message。換句話說,在接收到該Consumer的ack前,他它不會將新的Message分發給它。 設置方法如下:

????????????channel.basic_qos(prefetch_count=1)


一般情況下,我們只在消息分發的時候會去聲明channel.exchange_declare。作為好的習慣,在producer和consumer中分別聲明一次以保證所要使用的exchange存在

channel.exchange_declare(exchange='logs',

type='fanout')

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 來源 RabbitMQ是用Erlang實現的一個高并發高可靠AMQP消息隊列服務器。支持消息的持久化、事務、擁塞控...
    jiangmo閱讀 10,406評論 2 34
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 1. 歷史 RabbitMQ是一個由erlang開發的AMQP(Advanced Message Queue )的...
    高廣超閱讀 6,117評論 3 51
  • 我想 陪孩子學會游泳 我想 每年寒暑假都帶孩子出去旅游 我想 每天陪孩子吃晚飯 我想 讓孩子學會一門藝術 我想 每...
    hhllzwooooo閱讀 387評論 0 0
  • FC:formatting context 格式化上下文。css2.1定義了 BFC 塊級格式化上下文 + IFC...
    shanshanfei閱讀 989評論 0 0