一、訊息佇列
訊息佇列就是一種先進先出
的資料機構
當在分散式系統中的時候,不同的機器需要做資料互動,所以涉及到不同機器之間的資料互動,這樣的話就需要藉助專業的訊息佇列,常見的訊息佇列有 RabbitMQ 、Kafka...他們都是開源且支援語言較多。
訊息佇列解決的問題:
-
應用解耦
-
流量消峰:
如果訂單系統一秒最多能處理一萬次訂單,這個處理能力在平時綽綽有餘,正常時段我們下單一秒後就能返回結果。但是在高峰期,如果有兩萬次下單作業系統是處理不了的,只能限制訂單超過一萬後不允許使用者下單。
但是使用訊息佇列,就可以取消這個限制,把這一秒內的訂單放入佇列中分散成一段時間來處理,這樣使用者就可能在下單幾十秒後才能收到下單成功的操作,但是比不能下單要好。
-
訊息分發:
當A傳送一次訊息,B對訊息感興趣,就只需監聽訊息,C感興趣,C也去監聽訊息,而A完全不需要改動。
-
非同步訊息(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)