RabbitMQ
文章目錄
- RabbitMQ是由LShift提供的一個Advanced Message Queuing Protocol(AMQP)的開源實現,由以高性能、健壯以及可伸縮性出名的Erlang寫成,因此也是繼承了這些優點。
安裝
-
選擇RPM包下載,選擇對應平臺,本次安裝在CentOS7,其他平臺類似。https://www.rabbitmq.com/install-rpm.html
-
由於使用了erlang語言開發,所以需要erlang的包。erlang和RabbitMQ的兼容性,參考https://www.rabbitmq.com/which-erlang.html#compatibility-matrix
-
第二種錯誤,可以修改host文件,修改主機名即可
-
下載 rabbitmq-server-3.7.16-1.el7.noarch.rpm、erlang-21.3.8.6-1.el7.x86_64.rpm。socat在CentOS中源中有。
yum -y install erlang-21.3.8.6-1.el7.x86_64.rpm rabbitmq-server-3.7.16-1.el7.noarch.rpm
- 查看安裝的文件
[root@xdd ~]# rpm -ql rabbitmq-server
/etc/logrotate.d/rabbitmq-server
/etc/profile.d/rabbitmqctl-autocomplete.sh
/etc/rabbitmq
/usr/lib/ocf/resource.d/rabbitmq/rabbitmq-server
/usr/lib/ocf/resource.d/rabbitmq/rabbitmq-server-ha
/usr/lib/rabbitmq/autocomplete/bash_autocomplete.sh
/usr/lib/rabbitmq/autocomplete/zsh_autocomplete.sh
/usr/lib/rabbitmq/bin/cuttlefish
/usr/lib/rabbitmq/bin/rabbitmq-defaults
配置
環境配置
- 使用系統環境變量,如果沒有使用rabbitmq-env.conf中定義環境變量,否則使用缺省值
RABBITMQ_NODE_IP_ADDRESS the empty string, meaning that it should bind to all network interfaces.
RABBITMQ_NODE_PORT 5672
RABBITMQ_DIST_PORT RABBITMQ_NODE_PORT + 20000 #內部節點和客戶端工具通信用
RABBITMQ_CONFIG_FILE 配置文件路徑默認爲/etc/rabbitmq/rabbitmq
環境變量文件,可以不配置
工作特性配置文件
- rabbitmq.config配置文件
- 3.7支持新舊兩種配置文件格式
- erlang配置文件格式,爲了兼容繼續採用
2. sysctl格式,如果不需要兼容,RabbitMQ鼓勵使用。 (這個文件也可以不配置)
插件管理
列出所有可用插件
rabbitmq-plugins list
- 啓動WEB管理插件,會依賴啓用其他幾個插件。
[root@xdd rabbitmq]$ rabbitmq-plugins enable rabbitmq_management
啓動服務
systemctl start rabbitmq-server
- 啓動中,可能出現下面的錯誤
Error when reading /var/lib/rabbitmq/.erlang.cookie:eacces
這就是這個文件的權限問題,修改屬組、屬組爲rabbitmq即可
chown rabbitmq.rabbitmq /var/lib/rabbitmq/.erlang.cookie
- 服務啓動成功
[root@xdd ~]# ss -tanl | grep 5672
LISTEN 0 128 *:25672 *:*
LISTEN 0 128 *:15672 *:*
LISTEN 0 128 :::5672 :::*
[root@xdd ~]#
用戶管理
-
開始登陸WEB界面,
http://192.168.61.108(rabbitmq所在主機的ip):15672
-
使用guest/guest只能本地登陸,遠程登錄會報錯
- rabbitmqctl命令
rabbitmqctl [-n <node>] [-1][-q] <command> [<command options>]
- General options:
-n
node-q
,–quiet-t
,–timeout timeout-l
longnames
- Commands:
add_user <username> <password>
添加用戶list_user
列出用戶delete_user username
刪除用戶change_password <username> <password>
修改用戶名,密碼set_user_tags <username> <tag> [...]
設置用戶tag- list_user_permissions 列出用戶權限
-
添加用戶:
rabbitmqctl add_user username password
-
刪除用戶:
rabbitmqctl delete_user username
-
更改密碼:
rabbitmqctl change_password username newpassword
-
設置權限Tags,其實就是分配組:
rabbitmqctl set_user_tags username tag
-
設置xdd用戶爲管理員tag後登陸
# rabbitmqctl add_user gdy gdy #添加xdd用戶
# rabbitmqctl list_users #查看所有用戶
# rabbitmqctl set_user_tags gdy administrator #設置xdd用戶爲管理員用戶
-
tag的意義如下:
- administrator可以管理用戶、權限、虛擬主機。
- administrator可以管理用戶、權限、虛擬主機。
-
基本信息(web管理端口15672,協議端口5672)
-
虛擬主機
- 缺省虛擬主機,默認只能是guest用戶在本機鏈接,下圖新建的用戶gdy默認無法訪問任何虛擬主機
- 缺省虛擬主機,默認只能是guest用戶在本機鏈接,下圖新建的用戶gdy默認無法訪問任何虛擬主機
Pika庫
- Pika是純Python實現的支持AMQP協議的庫
pip install pika
RabbitMQ工作原理及應用
工作模式
- 名詞解釋
名詞 | 說明 |
---|---|
Server | 服務器 接受客戶端連接,實現消息隊列及路由功能的進程(服務),也稱爲消息代理 注意:客戶端可用生產者,也可以是消費者,它們都需要連接到Server |
Connection | 網絡物理連接 |
Channel | 一個連接允許多個客戶端連接 |
Exchange | 交換器。接收生產者發來的消息,決定如何路由給服務器中的隊列。 常用的類型有: direct(point-to-point) topic(publish-subscribe) fanout(multicast) |
Message | 消息 |
Message Queue | 消息隊列,數據的存儲載體 |
Bind | 綁定 建立消息隊列和交換器之間的關係,也就是說交換器拿到數據,把什麼樣的數據送給哪個隊列 |
Virtual Host | 虛擬主機 一批交換器、消息隊列和相關對象的集合。爲了多用戶互不干擾,使用虛擬主機分組交換機,消息隊列 |
Topic | 主題、話題 |
Broker | 可等價爲Server |
1.隊列
- 這種模式就是最簡單的生產者消費者模型,消息隊列就是一個FIFO的隊列
- 生產者send.py,消費者receie.py
- 官方例子:https://www.rabbitmq.com/tutorials/tutorial-one-python.html
- 注意:出現如下運行結果
pika.exceptions.ProbableAuthenticationError: (403, 'ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile.')
- 訪問被拒絕,還是權限問題,原因還是guest用戶只能訪問localhost上的缺省虛擬主機
- 解決辦法
- 缺省虛擬主機,默認只能在本機訪問,不要修改爲遠程訪問,是安全的考慮。
- 因此,在Admin中Virtual hosts中,新建一個虛擬主機test。
- 注意:新建的test虛擬主機的Users是誰,本次是gdy用戶
-
在ConnectionParameters中沒有用戶名、密碼填寫的參數,它使用參數credentials傳入,這個需要構建一個pika.credentials.Credentials對象。
-
參照官方例子,寫一個小程序
# send.py
import pika
from pika.adapters.blocking_connection import BlockingChannel
#構建用戶名密碼對象
credential = pika.PlainCredentials("gdy","gdy")
# 配置鏈接參數
params = pika.ConnectionParameters(
"192.168.61.108",#ip地址
5672, #端口
"test",#虛擬機
credential #用戶名密碼
)
# # 第二種建立連接方式
# params = pika.URLParameters("amqp://gdy:[email protected]:5672/test")
# 建立連接
connection = pika.BlockingConnection(params)
with connection:
# 建立通道
channel:BlockingChannel = connection.channel()
#創建一個隊列,queue命名爲hello,如果queue不存在,消息將被dropped
channel.queue_declare(queue="hello")
channel.basic_publish(
exchange="",#使用缺省exchange
routing_key="hello", #routing_key必須指定,這裏要求和目標queue一致
body="Hello world" #消息
)
print("消息發送成功Sent Message OK")
-
測試通過。去服務管理界面查看Exchanges和Queues。
-
URLParameters,也可以使用URL創建參數
# amqp://username:password@host:port/<virtual_host>[?query-string]
parameters = pika.URLParameters('amqp://guest:guest@rabbit-server1:5672/%2F')
# %2F指代/,就是缺省虛擬主機
- queue_declare聲明一個queue,有必要可以創建。
- basic_publish exchange爲空就使用缺省exchange,如果找不到指定的exchange,拋異
- 使用缺省exchange,就必須指定routing_key,使用它找到queue
- 修改上面生產者代碼,讓生產者連續發送send Message。在web端查看Queues中Ready的變化
# send.py
import pika
from pika.adapters.blocking_connection import BlockingChannel
import time
# 第二種建立連接方式
params = pika.URLParameters("amqp://gdy:[email protected]:5672/test")
# 建立連接
connection = pika.BlockingConnection(params)
with connection:
# 建立通道
channel:BlockingChannel = connection.channel()
#創建一個隊列,queue命名爲hello,如果queue不存在,消息將被dropped
channel.queue_declare(queue="hello")
for i in range(40):
channel.basic_publish(
exchange="",#使用缺省exchange
routing_key="hello", #routing_key必須指定,這裏要求和目標queue一致
body="data{:02}".format(i) #消息
)
time.sleep(0.5)
print("消息發送成功Sent Message OK")
- 構建receive.py消費者代碼
- 單個消費消息
- BlockingChannel.basic_get(“queue名稱”,是否阻塞)->(method,props,body)
- body爲返回的消息
- BlockingChannel.basic_get(“queue名稱”,是否阻塞)->(method,props,body)
# receie.py
import pika
from pika.adapters.blocking_connection import BlockingChannel
# 建立連接
params = pika.URLParameters("amqp://gdy:[email protected]:5672/test")
connection = pika.BlockingConnection(params)
with connection:
channel:BlockingChannel = connection.channel()
msg = channel.basic_get("hello",True) #從名稱爲hello的queue隊列中獲取消息,獲取不到阻塞
method,props,body = msg #拿不到的消息tuple爲(None,None,None)
if body:
print("獲取到了一個消息Get A message = {}".format(body))
else:
print("沒有獲取到消息empty")
- 獲取到消息後msg的結構如下:
(<Basic.GetOk(['delivery_tag=1', 'exchange=', 'message_count=38', 'redelivered=False', 'routing_key=hello'])>, <BasicProperties>, b'data01')
返回元組:(方法method,屬性properties,消息body)
無數據返回:(None,None,None)
- 批量消費消息recieve.py
# receie.py 消費代碼
import pika
from pika.adapters.blocking_connection import BlockingChannel
# 建立連接
params = pika.URLParameters("amqp://gdy:[email protected]:5672/test")
connection = pika.BlockingConnection(params)
def callback(channel,method,properties,body):
print("Get a message = {}".format(body))
with connection:
channel:BlockingChannel = connection.channel()
channel.basic_consume(
"hello",#隊列名
callback,#消費回調函數
True,#不迴應
)
print("等待消息,退出按CTRL+C;Waiting for messages. To exit press CTRL+C")
channel.start_consuming()
2.工作隊列
- 繼續使用隊列模式的生產者消費者代碼,啓動2個消費者。觀察結果,可以看到,2個消費者是交替拿到不同的消息。
- 這種工作模式時一種競爭工作方式,對某一個消息來說,只能有一個消費者拿走它。
- 從結果知道,使用的是輪詢方式拿走數據的。
- 注意:雖然上面的圖中沒有畫出exchange。用到缺省exchange。
3.發佈、訂閱模式(Publish/Subscribe)
- Publish/Subscribe發佈訂閱,想象一下訂閱者(消費者)訂閱這個報紙(消息),都應該拿到一份同樣內容的報紙。
- 訂閱者和消費者之間還有一個exchange,可以想象成郵局,消費者去郵局訂閱報紙,報社發報紙到郵局,郵局決定如何投遞到消費者手中。
- 上例子中工作隊列模式的使用,相當於,每個人只能拿到不同的報紙。所以不適合發佈訂閱模式。
- 當模式的exchange的type是fanout,就是一對多,即廣播模式。
- 注意,同一個queue的消息只能被消費一次,所以,這裏使用了多個queue,相當於爲了保證不同的消費者拿到同樣的數據,每一個消費者都應該有自己的queue。
# 生成一個交換機
channel.exchange_declare(
exchange="logs", #新交換機
exchange_type="fanout" #廣播
)
- 生產者使用廣播模式。在test虛擬主機中構建了一個logs交換機
- 至於queue,可以由生產者創建,也可以由消費者創建。
- 本次採用使用消費者端創建,生產者把數據都發往交換機logs,採用了fanout,然後將數據通過交換機發往已經綁定到此交換機的所有queue。
- 綁定Bingding,建立exchange和queue之間的聯繫
# 消費者端
result =channel.queue_declare(queue="") #生成一個隨機名稱的queue
resutl = channel.queue_declare(queue="",exclusive=True) #生成一個隨機名稱的queue,並在斷開鏈接時刪除queue
# 生成queue
q1:Method = channel.queue_declare(queue="",exclusive=True)
q2:Method = channel.queue_declare(queue="",exclusive=True)
q1name = q1.method.queue #可以通過result.method.queue 查看隨機名稱
q2name = q2.method.queue
print(q1name,q2name)
#綁定
channel.queue_bind(exchange="logs",queue=q1name)
channel.queue_bind(exchange="logs",queue=q2name)
- 生成者代碼
- 注意觀察 交換機和隊列
# send.py 生產者代碼
import pika
from pika.adapters.blocking_connection import BlockingChannel
import time
# 建立連接
params = pika.URLParameters("amqp://gdy:[email protected]:5672/test")
connection = pika.BlockingConnection(params)
channel:BlockingChannel = connection.channel()
with connection:
#指定交換機和模式
channel.exchange_declare(
exchange="logs",#新交換機
exchange_type="fanout" #扇出,廣播
)
for i in range(40):
channel.basic_publish(
exchange="logs",#使用指定的exhcange
routing_key="", #廣播模式,不指定routing_key
body = "data-{:02}".format(i) #消息
)
time.sleep(0.01)
print("消息發送完成")
- 特別注意:如果先開啓生產者,由於沒有隊列queue,請觀察數據
- 消費者代碼
- 構建queue並綁定到test虛擬機的logs交換機上
# receie.py 消費者代碼
import time
import pika
from pika.adapters.blocking_connection import BlockingConnection
from pika.adapters.blocking_connection import BlockingChannel
connection:BlockingConnection = pika.BlockingConnection(pika.URLParameters("amqp://gdy:[email protected]:5672/test"))
channel:BlockingChannel = connection.channel()
# 指定交換機
channel.exchange_declare(exchange="logs",exchange_type="fanout")
q1 = channel.queue_declare(queue="",exclusive=True)
q2 = channel.queue_declare(queue="",exclusive=True)
name1 = q1.method.queue #隊列名
name2 = q2.method.queue
#爲交換機綁定queue
channel.queue_bind(exchange="logs",queue=name1)
channel.queue_bind(exchange="logs",queue=name2)
def callback(channel,method,properties,body):
print("{}\n{}".format(channel,method))
print("獲取了一個消息 Get a message = {}".format(body))
with connection:
#爲第一個隊列綁定消費者函數
channel.basic_consume(
name1,#隊列名
callback, #消費者回調函數
True #不迴應
)
#爲第二個隊列綁定消費者函數
channel.basic_consume(name2,callback,True)
print("等待消息,退出按CTRL+C;Waiting for messages. To exit press CTRL+C")
channel.start_consuming()
- 先啓動消費者receie.py可以看到已經創建了exchange
- 如果exchange是fanout,也就是廣播了,routing_key就不用關心了。
q1 = channel.queue_declare(queue="",exclusive=True)
q2 = channel.queue_declare(queue="",exclusive=True)
- 注意:演示時要先啓動消費者,再啓動生產。如果先嚐試啓動生產者,在啓動消費者會導致部分數據丟失。因爲:exchange收了數據,沒有queue接受,所以,exchange丟棄了這些數據。
4.路由模式Routing
- 路由其實就是生產者的數據經過exhange的時候,通過匹配規則,決定數據的去向。
- 生產者代碼,交換機類型爲direct,指定路由的key
# send生產者
import time
import pika
import random
from pika.adapters.blocking_connection import BlockingConnection
from pika.adapters.blocking_connection import BlockingChannel
exchangename = "color"
colors = ("orange","black","green")
#建立連接
connection:BlockingConnection = pika.BlockingConnection(pika.URLParameters("amqp://gdy:[email protected]:5672/test"))
channel:BlockingChannel = connection.channel()
with connection:
channel.exchange_declare(
exchange=exchangename,#使用指定的exchange
exchange_type="direct" #路由模式
)
for i in range(40):
rk = random.choice(colors)
msg = "{}-data-{:02}".format(rk,i)
channel.basic_publish(
exchange=exchangename,#
routing_key=rk,#指定routing_key
body=msg #消息
)
print(msg,"----")
time.sleep(0.01)
print("消息發送完成 Sent ok")
- 消費者代碼
# receie.py消費者
import time
import pika
import random
from pika.adapters.blocking_connection import BlockingConnection
from pika.adapters.blocking_connection import BlockingChannel
exchangename = "color"
colors = ("orange","black","green")
#建立連接
connection:BlockingConnection = pika.BlockingConnection(pika.URLParameters("amqp://gdy:[email protected]:5672/test"))
channel:BlockingChannel = connection.channel()
channel.exchange_declare(exchange=exchangename,exchange_type="direct")
# 生成隊列,名稱隨機,exclusive=True斷開刪除該隊列
q1 = channel.queue_declare(queue="",exclusive=True)
q2 = channel.queue_declare(queue="",exclusive=True)
name1 = q1.method.queue #查看隊列名
name2 = q2.method.queue
print(name1,name2)
#綁定到交換機,而且一定要綁定routing_key
channel.queue_bind(exchange=exchangename,queue=name1,routing_key=colors[0])
channel.queue_bind(exchange=exchangename,queue=name2,routing_key=colors[1])
channel.queue_bind(exchange=exchangename,queue=name2,routing_key=colors[2])
def callback(channel,method,properties,body):
print("{}\n{}".format(channel,method))
print("獲取了一個消息get a message = {}".format(body))
print()
with connection:
channel.basic_consume(
name1,#隊列名
callback, #消息回調函數
True #不迴應
)
channel.basic_consume(name2,callback,True)
print("等待消息,退出按CTRL+C;Waiting for messages. To exit press CTRL+C")
channel.start_consuming()
-
注意:如果routing_key設置一樣,綁定的時候指定routing_key=‘black’,如下圖。和fanout就類似了,都是1對多,但是不同。
- 因爲fanout時,exchange不做數據過濾,1個消息,所有綁定的queue都會拿到一個副部。
- direct時候,要按照routing_key分配數據,上圖的black有2個queue設置了,就會把1個消息分發給這2個queue。
5.Topic話題
- Topic就是更加高級的路由,支持模式匹配而已。
- Topic的routing_key必須使用
.
點號分割的單詞組成。最多255個字節。 - 支持使用通配符:
*
表示嚴格的一個單詞#
表示0個或多個單詞
- 如果queue綁定的routing_key只是一個
#
,這個queue其實可以接收所有的消息。 - 如果沒有使用任何通配符,效果類似於direct,因爲只能和字符串匹配了。
- 生產者代碼
# send.py生產者代碼
import time
import pika
import random
from pika.adapters.blocking_connection import BlockingConnection
from pika.adapters.blocking_connection import BlockingChannel
exchangename = "products"
#產品和顏色搭配
colors = ("orange","black","green")
topics = ("phone.*","*.red") #兩種話題
product_type = ("phone","pc","tv") #3種產品
#建立連接
connection:BlockingConnection = pika.BlockingConnection(pika.URLParameters("amqp://gdy:[email protected]:5672/test"))
channel:BlockingChannel = connection.channel()
#指定交換機爲話題模式
channel.exchange_declare(exchange=exchangename,exchange_type="topic")
with connection:
for i in range(40):
rk = "{}.{}".format(random.choice(product_type),random.choice(colors))
msg = "{}-data-{:02}".format(rk,i)
channel.basic_publish(
exchange=exchangename,#使用指定的exchange
routing_key=rk,#指定routing_key
body=msg #消息
)
print(msg,"-----")
time.sleep(0.5)
print("消息發送完成 Sent ok")
- 消費者代碼
# recieve.py 消費者代碼
import time
import pika
import random
from pika.adapters.blocking_connection import BlockingConnection
from pika.adapters.blocking_connection import BlockingChannel
# 虛擬機名稱
exchangename = "products"
#建立連接
connection:BlockingConnection = pika.BlockingConnection(pika.URLParameters("amqp://gdy:[email protected]:5672/test"))
channel:BlockingChannel = connection.channel()
#指定虛擬機,交換機爲話題模式
channel.exchange_declare(exchange=exchangename,exchange_type="topic")
# 生成隊列,名稱隨機,exclusive=True斷開刪除該隊列
q1 = channel.queue_declare(queue="",exclusive=True)
q2 = channel.queue_declare(queue="",exclusive=True)
name1 = q1.method.queue #查看隊列名
name2 = q2.method.queue
print(name1,name2)
#綁定到交換機,而且一定綁定routing_key
#q1只收集phone開頭的routing_key的消息,也就是說只管收集類型信息
channel.queue_bind(exchange=exchangename,queue=name1,routing_key="phone.*")
# q2只收集red結尾的routing_key的消息,也就是說只管紅色的信息
channel.queue_bind(exchange=exchangename,queue=name2,routing_key="*.red")
def callback(channel,method,properties,body):
print("{}\n{}".format(channel,method))
print("獲取了一個消息Get a message = {}".format(body))
print()
with connection:
channel.basic_consume(
name1,#隊列名
callback,#消息回調函數
True #不迴應
)
channel.basic_consume(name2,callback,True)
print("等待消息,退出按CTRL+C;Waiting for messages. To exit press CTRL+C")
channel.start_consuming()
-
觀察消費者拿到的數據,注意觀察phone.red的數據出現次數。
-
由此,可以知道交換機在路由消息的時候,只要和queue的routing_key匹配,就把消息發給該queue。
RPC遠程過程調用
- RabbitMQ的RPC的應用場景較少,因爲有更好的RPC通信框架。
消息隊列的作用
- 系統間解耦
- 解決生產者、消費者速度匹配
- 由於稍微上規模的項目都會分層、分模塊開發,模塊間或系統間儘量不要直接耦合,需要開放公共接口提供給別的模塊或系統調用,而調用可能觸發併發問題,爲了緩衝和解耦,往往採用中間件技術。
- RabbitMQ只是消息中間件中的一種應用程序,也是較常用的中間件服務。