RabbitMQ基本使用

雲崖先生發表於2021-04-09

RabbitMQ

RabbitMQ可以說是目前較為火熱的一款訊息中介軟體,其本身由Erlang語言進行編寫,部署簡單操作方便,是必備的一門技術棧。

RabbitMQ官網

它支援各種主流語言的驅動,如下所示:

image-20210409172537673

那麼現在本章將用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服務
  • 生產者/消費者建立訊息佇列
  • 生產者產生訊息,放入訊息佇列中
  • 消費者獲得訊息,並且消費該訊息

image-20210409183509514

生產者程式碼如下:

#!/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()

多個消費

如果僅有一個生產者,而有多個消費者想要獲取資料,那這些消費者則會輪詢的依次的從佇列中獲得資料,如下程式碼可對其進行驗證,你只需要並行的多開幾個消費者即可:

image-20210409184725668

生產者程式碼 如下:

#!/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服務傳送一個我以接收,你可以從佇列中將該訊息抹除的訊號。

如下圖所示:

image-20210409185148313

如果該引數設定為False,則代表消費者向RabbitMQ的這條ack確認訊號轉為手動觸發,也就是說,我們可以在消費者成功的消費掉這條資訊後再手動通知RabbitMQ從佇列中將該訊息進行移除。

本質上,該引數如果為False,消費者是不會取出佇列中的資訊,而是完全拷貝一份。

image-20210409185504961

在消費完成後,你可以手動通知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個消費者,你會發現佇列中的訊息不是輪詢了,而是被第一個消費者獨佔:

image-20210409190036510

持久化引數

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中的有所不同,它必須依賴一個被稱為交換機的東西來進行訊息的釋出,整個流程如下:

  • 生產者建立交換機
  • 消費者建立佇列連結至交換機
  • 生產者建立訊息,放入交換機中
  • 消費者通過佇列拿出交換機中的訊息

如下圖所示:

image-20210409195346859

不同於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()

關鍵字訂閱

在上面的普通釋出訂閱模式中,只要生產者生產了資料,消費者就必須接收。

而在關鍵字訂閱中,消費者可以篩選交換機中的資料,如下圖所示:

image-20210409200641433

我們需要做的是改變交換機的型別為關鍵字型別,並且指定消費者所關心的資料關鍵字。

注意!必須先啟動消費者,再啟動生產者

生產者程式碼如下:

#!/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()

模糊訂閱

模糊訂閱是關鍵字訂閱的一種升級版。

關鍵字訂閱的資訊必須歸於某一型別,關鍵字一個不能多一個不能少,比如我繫結了國家這個關鍵字,那麼就只能匹配國家的資訊。

而對於國家.天氣、國家.新聞這種資訊一概不會匹配。

而模糊訂閱就可以做到關鍵字訂閱做不到的,我們可以使用萬用字元*以及#來對關鍵字進行模糊匹配。

  • *是指僅匹配後面的任意的一個字元
  • #是指匹配後面的連續多個字元

現在,我們可以使用國家.#來匹配到任何關於國家的詞彙,如國家天氣、國家新聞等等資訊。

如下圖所示:

image-20210409205919907

注意!必須先啟動消費者,再啟動生產者

生產者程式碼如下:

#!/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()

相關文章