RabbitMQ
RabbitMQ可以說是目前較為火熱的一款訊息中介軟體,其本身由Erlang語言進行編寫,部署簡單操作方便,是必備的一門技術棧。
它支援各種主流語言的驅動,如下所示:
那麼現在本章將用Python來探究一下RabbitMQ的使用。
RabbitMQ官方提供多種安裝方式,具體可參照官網,這裡將採用Docker部署,版本為3.8.14:
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management
同時我們還需要為Python安裝對應操縱RabbitMQ的驅動模組,名為pika,可直接通過pip進行安裝:
pip3 install pika
基礎的p2p
簡單模式
基礎的p2p在RabbitMQ中被稱為簡單模式,即一個生產者的資訊僅能被一個消費者所接收,整個流程步驟如下:
- 生產者/消費者連結RabbitMQ服務
- 生產者/消費者建立訊息佇列
- 生產者產生訊息,放入訊息佇列中
- 消費者獲得訊息,並且消費該訊息
生產者程式碼如下:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import pika
# 建立連結
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
# 拿到操縱物件
channel = connection.channel()
# 建立/獲取佇列
channel.queue_declare(queue="q1")
# exchange = "": 普通的p2p模式
# routing_key:放進那個佇列
# body:訊息主體
channel.basic_publish(
exchange="",
routing_key="q1",
body="this is a message",
)
print("The message is sent to q1!")
消費者程式碼如下:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import pika
# 建立連結
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
# 拿到操縱物件
channel = connection.channel()
# 建立/獲取佇列
channel.queue_declare(queue="q1")
# 回撥函式:ch,method,properties都是固定寫法,body引數是訊息體,bytes格式
def callback(ch, method, properties, body):
print(body.decode("utf8"))
print("The consumer successfully gets the message from the q1 queue!")
# queue:監聽的佇列
# auto_ack:自動回覆ack確認
channel.basic_consume(
queue="q1",
auto_ack=True,
on_message_callback=callback,
)
# 開始監聽佇列,會一直進行監聽
channel.start_consuming()
多個消費
如果僅有一個生產者,而有多個消費者想要獲取資料,那這些消費者則會輪詢的依次的從佇列中獲得資料,如下程式碼可對其進行驗證,你只需要並行的多開幾個消費者即可:
生產者程式碼 如下:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
channel = connection.channel()
channel.queue_declare(queue="q1")
for i in range(5):
channel.basic_publish(
exchange="",
routing_key="q1",
body="this is a message{0}".format(i),
)
print("The message{0} is sent to q1".format(i))
消費者程式碼如下:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
channel = connection.channel()
channel.queue_declare(queue="q1")
def callback(ch, method, properties, body):
print(body.decode("utf8"))
print("The consumer successfully gets the message from the q1 queue!")
channel.basic_consume(
queue="q1",
auto_ack=True,
on_message_callback=callback,
)
channel.start_consuming()
相關引數
應答引數
在消費者中,有一條這樣的程式碼:
# auto_ack=True
channel.basic_consume(
queue="q1",
auto_ack=True,
on_message_callback=callback,
)
這條程式碼的意思是一旦消費者從佇列中取出訊息,不論是否消費該訊息,都會立即向RabbitMQ服務傳送一個我以接收,你可以從佇列中將該訊息抹除的訊號。
如下圖所示:
如果該引數設定為False,則代表消費者向RabbitMQ的這條ack確認訊號轉為手動觸發,也就是說,我們可以在消費者成功的消費掉這條資訊後再手動通知RabbitMQ從佇列中將該訊息進行移除。
本質上,該引數如果為False,消費者是不會取出佇列中的資訊,而是完全拷貝一份。
在消費完成後,你可以手動通知RabbitMQ刪除訊息的程式碼如下,固定寫法:
ch.basic_ack(delivery_tag=method.delivery_tag)
還是上一個整體的消費者程式碼吧...
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import time
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
channel = connection.channel()
channel.queue_declare(queue="q1")
def callback(ch, method, properties, body):
print("Processing...")
time.sleep(3)
# 通知RabbitMQ,你可以刪除了
ch.basic_ack(delivery_tag=method.delivery_tag)
# auto_ack:手動回覆ack確認
channel.basic_consume(
queue="q1",
auto_ack=False,
on_message_callback=callback,
)
channel.start_consuming()
另外,如果此時你啟動3個消費者,你會發現佇列中的訊息不是輪詢了,而是被第一個消費者獨佔:
持久化引數
RabbitMQ中所有的訊息都儲存在記憶體中,這意味著某些特殊情況下,如RabbitMQ服務突然宕掉之後,在佇列中的資料都會丟失。
我們可以對佇列進行持久化設定,讓其將資料儲存在磁碟中。
有趣的是,RabbitMQ中對佇列的持久化分為2個層次:
- 你這個佇列要不要持久化?
- 你這個佇列中的訊息要不要持久化?
需要注意的是,在RabbitMQ的一次服務週期中,一個佇列如果已經宣告是非持久化佇列,則不能將其改變為持久化佇列,你需要重新建立一個新的持久化佇列。
用程式碼看一下實際效果吧,將下面這段生產者程式碼嘗試執行:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
channel = connection.channel()
# durable:如果為True則代表著是持久化佇列,預設是False
channel.queue_declare(queue="q2", durable=True)
# delivery_mode:2是對該訊息持久化,1是不持久化,預設為1
channel.basic_publish(
exchange="",
routing_key="q2",
body="持久化資訊",
properties=pika.BasicProperties(
delivery_mode=2,
)
)
channel.basic_publish(
exchange="",
routing_key="q2",
body="非持久化資訊",
)
print("The messages is sent to q2")
現在q2佇列中應該具有2條資訊,我們停止Docker容器的執行在對其重新進行啟動:
$ docker container stop rabbitmq
$ docker container start rabbitmq
然後啟動消費者,看能拿到幾條資訊:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import time
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
channel = connection.channel()
# durable:如果為True則代表著是持久化佇列,預設是False
channel.queue_declare(queue="q2", durable=True)
def callback(ch, method, properties, body):
print(body.decode("utf8"))
channel.basic_consume(
queue="q2",
auto_ack=True,
on_message_callback=callback,
)
channel.start_consuming()
當然,結果只能拿到持久化資訊,非持久化資訊是拿不到的。
閒置消費
預設的佇列訊息分發策略是輪詢分發,這會導致一個問題,如我有2個消費者:
- 消費者A拿出訊息,並處理
- 消費者B拿出訊息,並處理
- 消費者A想拿出訊息,但是消費者B還沒有處理完,消費者A拿不出訊息
所以我們可以將分發策略改為閒置消費,即誰處理的快,下一條訊息就歸誰,而不再使用輪詢分發,你只需要在消費者的下面加上這句程式碼即可。
channel.basic_qos(prefetch_count=1)
還是拿多個消費一節的例子來舉例,修改一下消費者的程式碼,生產者依舊用上面的即可:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import time
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
channel = connection.channel()
channel.queue_declare(queue="q1")
def callback(ch, method, properties, body):
print(body.decode("utf8"))
# 第二個消費者取消註釋
time.sleep(50)
ch.basic_ack(delivery_tag=method.delivery_tag)
# 關閉輪詢策略,改為閒置優先,必須寫在監聽的上面
channel.basic_qos(prefetch_count=1)
channel.basic_consume(
queue="q1",
auto_ack=False,
on_message_callback=callback,
)
channel.start_consuming()
交換機模式
普通釋出訂閱
RabbitMQ中的釋出訂閱與Kafka中的有所不同,它必須依賴一個被稱為交換機的東西來進行訊息的釋出,整個流程如下:
- 生產者建立交換機
- 消費者建立佇列連結至交換機
- 生產者建立訊息,放入交換機中
- 消費者通過佇列拿出交換機中的訊息
如下圖所示:
不同於p2p模式,交換機模式下所有監聽該交換機的佇列都會獲取到資訊,並且傳遞給消費者。
注意!必須先啟動消費者,再啟動生產者
生產者程式碼如下:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
channel = connection.channel()
# 建立交換機
# exchange:交換機的名字
# exchange_type:交換機的型別,普通的釋出訂閱模式
channel.exchange_declare(
exchange="switch",
exchange_type="fanout",
)
# exchange = "switch": 向交換機中傳送訊息
# routing_key:訊息關鍵字
# body:訊息主體
for i in range(5):
channel.basic_publish(
exchange="switch",
routing_key="",
body="this is a message{0}".format(i),
)
print("The message{0} is sent to switch".format(i))
消費者程式碼如下:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import time
import pika
# 建立連結
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
# 拿到操縱物件
channel = connection.channel()
# 監聽的交換機
# exchange:交換機的名字
# exchange_type:交換機的型別,普通型別(釋出訂閱)
channel.exchange_declare(
exchange="switch",
exchange_type="fanout",
)
# 建立一個用於去交換機中獲取訊息的佇列
# exclusive:佇列名隨機
# result:建立結果
result = channel.queue_declare("", exclusive=True)
# 從建立結果中獲取佇列名
queue_name = result.method.queue
# 佇列繫結交換機
channel.queue_bind(
exchange="switch",
queue=queue_name
)
# 回撥函式:ch,method,properties都是固定寫法,body引數是訊息體,bytes格式
def callback(ch, method, properties, body):
print(body.decode("utf8"))
# queue:監聽的佇列
# auto_ack:自動回覆ack確認
channel.basic_consume(
queue=queue_name,
auto_ack=True,
on_message_callback=callback,
)
# 開始監聽佇列
channel.start_consuming()
關鍵字訂閱
在上面的普通釋出訂閱模式中,只要生產者生產了資料,消費者就必須接收。
而在關鍵字訂閱中,消費者可以篩選交換機中的資料,如下圖所示:
我們需要做的是改變交換機的型別為關鍵字型別,並且指定消費者所關心的資料關鍵字。
注意!必須先啟動消費者,再啟動生產者
生產者程式碼如下:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import pika
# 建立連結
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
# 拿到操縱物件
channel = connection.channel()
# 建立交換機
# exchange:交換機的名字
# exchange_type:交換機的型別,關鍵字釋出訂閱模式
channel.exchange_declare(
exchange="switch1",
exchange_type="direct",
)
# exchange = "switch1": 向交換機中傳送訊息
# routing_key:訊息關鍵字
# body:訊息主題
for i in range(3):
li1 = ["新聞", "天氣", "國家"]
li2 = ["大新聞", "好天氣", "某國家成立了"]
channel.basic_publish(
exchange="switch1",
routing_key=li1[i],
body=li2[i],
)
print("The message{0} is sent to switch1".format(i))
消費者程式碼如下,僅能接收到大新聞:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import time
import pika
# 建立連結
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
# 拿到操縱物件
channel = connection.channel()
# 監聽的交換機
# exchange:交換機的名字
# exchange_type:交換機的型別,關鍵字釋出訂閱模式
channel.exchange_declare(
exchange="switch1",
exchange_type="direct",
)
# 建立一個用於去交換機中獲取訊息的佇列
# exclusive:佇列名隨機
# result:建立結果
result = channel.queue_declare("", exclusive=True)
# 從建立結果中獲取佇列名
queue_name = result.method.queue
# 佇列繫結交換機,僅獲取新聞相關的
channel.queue_bind(
exchange="switch1",
queue=queue_name,
routing_key="新聞",
)
# 回撥函式:ch,method,properties都是固定寫法,body引數是訊息體,bytes格式
def callback(ch, method, properties, body):
print(body.decode("utf8"))
# queue:監聽的佇列
# auto_ack:自動回覆ack確認
channel.basic_consume(
queue=queue_name,
auto_ack=True,
on_message_callback=callback,
)
# 開始監聽佇列
channel.start_consuming()
模糊訂閱
模糊訂閱是關鍵字訂閱的一種升級版。
關鍵字訂閱的資訊必須歸於某一型別,關鍵字一個不能多一個不能少,比如我繫結了國家這個關鍵字,那麼就只能匹配國家的資訊。
而對於國家.天氣、國家.新聞這種資訊一概不會匹配。
而模糊訂閱就可以做到關鍵字訂閱做不到的,我們可以使用萬用字元*以及#來對關鍵字進行模糊匹配。
- *是指僅匹配後面的任意的一個字元
- #是指匹配後面的連續多個字元
現在,我們可以使用國家.#來匹配到任何關於國家的詞彙,如國家天氣、國家新聞等等資訊。
如下圖所示:
注意!必須先啟動消費者,再啟動生產者
生產者程式碼如下:
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import pika
# 建立連結
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
# 拿到操縱物件
channel = connection.channel()
# 建立交換機
# exchange:交換機的名字
# exchange_type:交換機的型別,模糊的訂閱模式
channel.exchange_declare(
exchange="switch3",
exchange_type="topic",
)
# exchange = "switch3": 向交換機中傳送訊息
# routing_key:訊息關鍵字,必須嚴格按照.進行分割才能匹配
# body:訊息主體
channel.basic_publish(
exchange="switch3",
routing_key="國家.新聞",
body="xx國家的新聞",
)
channel.basic_publish(
exchange="switch3",
routing_key="國家.天氣",
body="xx國家的天氣",
)
channel.basic_publish(
exchange="switch3",
routing_key="天氣.新聞",
body="xx天氣的新聞",
)
print("The messages is sent to switch3")
消費者程式碼如下,僅能接收到國家.新聞、國家.天氣,而對於天氣.新聞來說是接收不到的::
#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
import time
import pika
# 建立連結
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost", port=5672))
# 拿到操縱物件
channel = connection.channel()
# 監聽的交換機
# exchange:交換機的名字
# exchange_type:交換機的型別,模糊的訂閱模式
channel.exchange_declare(
exchange="switch3",
exchange_type="topic",
)
# 建立一個用於去交換機中獲取訊息的佇列
# exclusive:佇列名隨機
# result:建立結果
result = channel.queue_declare("", exclusive=True)
# 從建立結果中獲取佇列名
queue_name = result.method.queue
# 佇列繫結交換機,僅獲取國家xx相關的
channel.queue_bind(
exchange="switch3",
queue=queue_name,
routing_key="國家.#",
)
# 回撥函式:ch,method,properties都是固定寫法,body引數是訊息體,bytes格式
def callback(ch, method, properties, body):
print(body.decode("utf8"))
# queue:監聽的佇列
# auto_ack:自動回覆ack確認
channel.basic_consume(
queue=queue_name,
auto_ack=True,
on_message_callback=callback,
)
# 開始監聽佇列
channel.start_consuming()