RabbitMQ訊息佇列

Yang`發表於2021-07-29

一、訊息佇列

訊息佇列就是一種先進先出的資料機構

當在分散式系統中的時候,不同的機器需要做資料互動,所以涉及到不同機器之間的資料互動,這樣的話就需要藉助專業的訊息佇列,常見的訊息佇列有 RabbitMQ 、Kafka...他們都是開源且支援語言較多。

訊息佇列解決的問題:

  1. 應用解耦

  2. 流量消峰:

    如果訂單系統一秒最多能處理一萬次訂單,這個處理能力在平時綽綽有餘,正常時段我們下單一秒後就能返回結果。但是在高峰期,如果有兩萬次下單作業系統是處理不了的,只能限制訂單超過一萬後不允許使用者下單。

    但是使用訊息佇列,就可以取消這個限制,把這一秒內的訂單放入佇列中分散成一段時間來處理,這樣使用者就可能在下單幾十秒後才能收到下單成功的操作,但是比不能下單要好。

  3. 訊息分發:

    當A傳送一次訊息,B對訊息感興趣,就只需監聽訊息,C感興趣,C也去監聽訊息,而A完全不需要改動。

  4. 非同步訊息(Celery 就是對訊息佇列的分裝)

RabbitMQ 和 Kafka

RabbitMQ :吞吐量小,有訊息確認(對訊息可靠性有要求,就用它)

Kafka:吞吐量高,注重高吞吐量,不注重訊息的可靠性,資料量特別大

二、按裝RabbitMQ

1、原生安裝:

# 安裝擴充套件epel源
wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
    
yum -y install erlang			# 因為RabbitMQ是erlang語言開發的,所以要按裝
yum -y install rabbitmq-server	# 安裝RabbitMQ
systemctl start rabbitmq-server	# 啟動

# 建立使用者
rabbitmqctl add_user 使用者名稱 密碼
# 分配許可權
rabbitmqctl set_user_tags 使用者名稱 administrator ——>(設定使用者為管理員角色)
rabbitmqctl set_permissions -p "/" 使用者名稱 ".*" ".*" ".*"	# 設定許可權
systemctl reatart rabbitmq-server	# 重啟

2、docker拉取

docker pull rabbitmq:3.8.3-management		# 自動開啟了web管理介面

# 啟動需要配置使用者名稱和密碼
docker run -di --name rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 rabbitmq:3.8.3-management

5672:是RabbitMQ的預設埠
15672:web管理介面的埠

三、基本使用

生產者:

import pika

# 有使用者名稱密碼
credentials = pika.PlainCredentials('admin', 'admin')
# 拿到連線物件
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.88.131', credentials=credentials))

# 拿到channel物件
channel = connection.channel()

# 宣告一個佇列
channel.queue_declare(queue='test')

# 生產者向佇列中放入一條訊息
channel.basic_publish(exchange='',
                      routing_key='test',   # 指定向那個佇列放入
                      body='測試資料')        # 放入的內容
                      
# 關閉連線
connection.close()

消費者:

import pika

# 有使用者名稱密碼
credentials = pika.PlainCredentials('admin', 'admin')
# 拿到連線物件
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.88.131', credentials=credentials))

# 拿到channel物件
channel = connection.channel()

# 宣告一個佇列,如果消費者先起來,那麼就先宣告一個佇列
channel.queue_declare(queue='test')


def callback(ch, method, properties, body):
    print(f'測試:{body}')


# 消費者從指定的佇列中拿訊息消費,一旦有一條轉到 callback 裡
channel.basic_consume(queue='test', on_message_callback=callback, auto_ack=True)

# 阻塞主,一直等待拿訊息消費
channel.start_consuming()

四、確認機制

訊息確認機制其實就是消費者中 auto_ack 的設定

生產者不變

消費者:

import pika

# 有使用者名稱密碼
credentials = pika.PlainCredentials('admin', 'admin')
# 拿到連線物件
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.88.131', credentials=credentials))

# 拿到channel物件
channel = connection.channel()

# 宣告一個佇列,如果消費者先起來,那麼就先宣告一個佇列
channel.queue_declare(queue='test')


def callback(ch, method, properties, body):
    print(f'測試:{body}')
    # 如果auto_ack=False這樣設定後
    # 也可以這樣設定當真正的訊息處理完了,在發確認也是可以的
    ch.basic_ack(delivery_tag=method.delivery_tag)


# auto_ack=True,佇列收到確認,就會自動把消費過的訊息刪除。
# auto_ack=False,那麼就不會給佇列傳送確認訊息了,佇列就不會刪除訊息。不會自動回覆確認訊息,
channel.basic_consume(queue='test', on_message_callback=callback, auto_ack=False)

# 阻塞主,一直等待拿訊息消費
channel.start_consuming()

五、持久化

佇列持久化:就是在宣告佇列的時候,指定持久化durable=True,佇列必須是新的才可以

channel.queue_declare(queue='test', durable=True)	# test 佇列持久化

訊息持久化:就是在釋出訊息的時候新增

# 生產者向佇列中放入一條訊息
channel.basic_publish(exchange='',
                      routing_key='test',   # 指定向那個佇列放入
                      body='hello',         # 放入的內容
                      properties=pika.BasicProperties(delivery_mode=2) # 訊息持久化
                      )

六、閒置消費

當正常情況下如果有多個消費者,那麼就會按照順序第一個訊息給 第一個消費者,第二個訊息給第二個消費者

但是當第一個訊息的消費者處理資訊很耗時,一直沒有結束,那麼就可以讓第二個消費者優先獲取閒置訊息。

消費者:

import pika

# 有使用者名稱密碼
credentials = pika.PlainCredentials('admin', 'admin')
# 拿到連線物件
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.88.131', credentials=credentials))

# 拿到channel物件
channel = connection.channel()

# 宣告一個佇列,如果消費者先起來,那麼就先宣告一個佇列
channel.queue_declare(queue='test')


def callback(ch, method, properties, body):
    print(f'測試:{body}')


# 就只有這一句話,誰閒置誰獲取,沒必要按照順序一個一個來
channel.basic_qos(prefetch_count=1)

channel.basic_consume(queue='test', on_message_callback=callback, auto_ack=True)

# 阻塞主,一直等待拿訊息消費
channel.start_consuming()

七、釋出訂閱

釋出訂閱就是:我可以有多個訂閱者來訂閱你的訊息,這樣釋出者只需要釋出一條, 我的所有隻要訂閱你的人都可以消費你的訊息

模型:當我的訂閱者起來了之後,就會建立一個佇列,多個訂閱者就會建立多個佇列,當釋出者生產了訊息之後,會傳給 exchange ,然後 exchange 會把訊息複製分別分發到訂閱者建立的佇列中,這樣就實現了只要監聽你,那就能收到你發的訊息。

基本使用

釋出者:

import pika

# 有使用者名稱密碼
credentials = pika.PlainCredentials('admin', 'admin')
# 拿到連線物件
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.88.131', credentials=credentials))

# 拿到channel物件
channel = connection.channel()

# 不指定佇列,指定了 exchange 複製分發訊息
channel.exchange_declare(exchange='conn', exchange_type='fanout')

# 生產者向佇列中放入一條訊息
channel.basic_publish(exchange='conn',      # 指定複製分發訊息的 exchange
                      routing_key='',   	# 不設定指定向那個佇列放入
                      body='Hello Word',    # 放入的內容
                      )
# 關閉連線
connection.close()

訂閱者:啟動多次,都繫結到了同一個 exchange,所以就會都收到同一個 exchange 分發的訊息

import pika

# 有使用者名稱密碼
credentials = pika.PlainCredentials('admin', 'admin')
# 拿到連線物件
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.88.131', credentials=credentials))


# 拿到channel物件
channel = connection.channel()


# 宣告一個佇列,如果消費者先起來,那麼就先宣告一個佇列
channel.exchange_declare(exchange='conn', exchange_type='fanout')


# queue 不能制定名字,因為它們的名字都是不一樣的
result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue    # 生成一個隨機的 queue 名字



# 把隨機生成的佇列繫結到exchange上
channel.queue_bind(exchange='conn', queue=queue_name)


def callback(ch, method, properties, body):
    print(f'測試:{body}')


channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)

# 阻塞主,一直等待拿訊息消費
channel.start_consuming()

關鍵字

需要設定 exchange_type 的型別為 direct

並且在釋出訊息的時候設定多個關鍵字:routing_key

在訂閱者中也需要設定 exchange_type 的型別 direct

並且當訂閱者繫結 exchange 的時候也需要設定 routing_key,

這樣的話在釋出者釋出訊息後,exchange 會根據釋出者和訂閱者設定的 routing_key 進行匹配,當訂閱者的 routing_key 匹配上了釋出者的 routing_key 的話,那麼訂閱者就可以接收到釋出者釋出的訊息,反之收不到訊息。

釋出者:

import pika

# 有使用者名稱密碼
credentials = pika.PlainCredentials('admin', 'admin')
# 拿到連線物件
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.88.131', credentials=credentials))

# 拿到channel物件
channel = connection.channel()

# 不指定佇列,指定了 exchange 複製分發訊息,exchange_type='direct'
channel.exchange_declare(exchange='conn1', exchange_type='direct')

# 生產者向佇列中放入一條訊息
channel.basic_publish(exchange='conn1',     # 指定複製分發訊息的 exchange
                      routing_key='abc',   # 指定關鍵字
                      body='Hello Word',    # 放入的內容
                      )
# 關閉連線
connection.close()

消費者1:

import pika

# 有使用者名稱密碼
credentials = pika.PlainCredentials('admin', 'admin')
# 拿到連線物件
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.88.131', credentials=credentials))


# 拿到channel物件
channel = connection.channel()


# 宣告一個佇列,如果消費者先起來,那麼就先宣告一個佇列
channel.exchange_declare(exchange='conn', exchange_type='direct')


# queue 不能制定名字,因為它的名字都是不一樣的
result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue    # 生成一個隨機的 queue 名字
print(queue_name)


# 把隨機生成的佇列繫結到exchange上,
# 並設定routing_key='abc',也就是說只有釋出者的routing_key中包含有'abc',此訂閱者才會收到訊息
channel.queue_bind(exchange='conn1', queue=queue_name, routing_key='abc')


def callback(ch, method, properties, body):
    print(f'測試:{body}')


channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)

# 阻塞主,一直等待拿訊息消費
channel.start_consuming()

消費者2:

import pika

# 有使用者名稱密碼
credentials = pika.PlainCredentials('admin', 'admin')
# 拿到連線物件
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.88.131', credentials=credentials))


# 拿到channel物件
channel = connection.channel()


# 宣告一個佇列,如果消費者先起來,那麼就先宣告一個佇列
channel.exchange_declare(exchange='conn', exchange_type='direct')


# queue 不能制定名字,因為它的名字都是不一樣的
result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue    # 生成一個隨機的 queue 名字
print(queue_name)


# 把隨機生成的佇列繫結到exchange上,
# 並設定了多個routing_key,也就是說只有釋出者的routing_key中包含有入下兩個之一,此訂閱者都會收到訊息
channel.queue_bind(exchange='conn1', queue=queue_name, routing_key='abc')
channel.queue_bind(exchange='conn1', queue=queue_name, routing_key='abcd')


def callback(ch, method, properties, body):
    print(f'測試:{body}')


channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)

# 阻塞主,一直等待拿訊息消費
channel.start_consuming()

模糊匹配

在訂閱者繫結匹配的時候可以進行模糊匹配發布者的 routing_key ,匹配上了就能接收到釋出者釋出的訊息

# 表示後面可以跟任意字元
* 表示後面只能跟一個單詞

釋出者:

import pika

# 有使用者名稱密碼
credentials = pika.PlainCredentials('admin', 'admin')
# 拿到連線物件
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.88.131', credentials=credentials))

# 拿到channel物件
channel = connection.channel()

# 不指定佇列,指定了 exchange 複製分發訊息,exchange_type='topic'
channel.exchange_declare(exchange='conn1', exchange_type='topic')

# 生產者向佇列中放入一條訊息
channel.basic_publish(exchange='conn2',     # 指定複製分發訊息的 exchange
                      routing_key='abcdefg',    # 指定關鍵字
                      body='Hello Word',    # 放入的內容
                      )
# 關閉連線
connection.close()

訂閱者:

import pika

# 有使用者名稱密碼
credentials = pika.PlainCredentials('admin', 'admin')
# 拿到連線物件
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.88.131', credentials=credentials))


# 拿到channel物件
channel = connection.channel()


# 宣告一個佇列,如果消費者先起來,那麼就先宣告一個佇列
channel.exchange_declare(exchange='conn', exchange_type='direct')


# queue 不能制定名字,因為它的名字都是不一樣的
result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue    # 生成一個隨機的 queue 名字
print(queue_name)


# 把隨機生成的佇列繫結到exchange上,
# 並設定routing_key='abc#',也就是說只有釋出者的routing_key中包含有'abc'開頭,此訂閱者才會收到訊息
channel.queue_bind(exchange='conn2', queue=queue_name, routing_key='abc#')


def callback(ch, method, properties, body):
    print(f'測試:{body}')


channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)

# 阻塞主,一直等待拿訊息消費
channel.start_consuming()

八、python中的RPC框架

RPC :遠端過程呼叫

例如:兩個服務呼叫,服務1通過網路呼叫服務2的方法。

SimpleXMLRPCServer

自帶的:資料包大,速度慢

服務端:

from xmlrpc.server import SimpleXMLRPCServer


class RPCServer(object):

    def getObj(self):
        return 'get obj'

    def sendObj(self, data):
        return 'send obj'


# SimpleXMLRPCServer
server = SimpleXMLRPCServer(('localhost', 4242), allow_none=True)
server.register_introspection_functions()
server.register_instance(RPCServer())
server.serve_forever()

客戶端:

from xmlrpc.client import ServerProxy

client = ServerProxy('http://localhost:4242')
ret = client.getObj()
print(ret)

ZeroRPC

第三方的:底層使用 ZeroMQ 和 MessagePack ,速度快,響應時間短,併發高。

服務端:

import zerorpc

class RPCServer(object):

    def getObj(self):
        return 'get obj'

    def sendObj(self, data):
        return 'send obj'


server = zerorpc.Server(RPCServer())
server.bind('tcp://0.0.0.0:4243')   # 允許連線的
server.run()

客戶端:

import zerorpc

client = zerorpc.Client()
client.connect('tcp://127.0.0.1:4243')  # 連線
ret = client.getObj()
print(ret)

相關文章