为什么用
Python中的Queue模块可以实现进程间的通信,但这里的通信指的是父进程与子进程或同一个父进程的子进程之间的通信,若要想实现相互独立的进程之间的通信就要用到RabbitMQ
RabbitMQ类似JSON,不同的语言都何以使用json,json连接了不同语言之间的通信桥梁,(这里只是比喻方便理解)
安装
RabbitMQ使用erlang语言开发的,windows系统使用需要先安装erlang语言环境(安装时一直下一步即可)
erlang语言环境安装好后就可以安装RabbitMQ(安装时一直下一步即可)
装完之后系统服务里有RabbitMQ服务,把RabbitMQ服务启动
linux下安装好后 rabbitmq-server start 启动
erlang与rabbitMQ环境变量配置 MQ用户配置相关 操作存在的队列
使用
python使用RabbitMQ的模块有pika,celery,Halgha主流的是pika
pip3 install pika
这里所有的用法都是基于RabbitMQ是工作在‘localhost’上,并且端口号为15672,能在浏览器里访问http://localhost:15672这个地址。
简单消息分发(一对一)
这就是RabbitMQ最简单的工作模式,p为生产者(Producer),生产者发送message给queue,queue再把消息发送至消费者c(Customer)
- Producers.py
import pika
#1. 建立连接
connect = pika.BlockingConnection(
pika.ConnectionParameters('localhost')
)
# 建立管道
channel = connect.channel()
# 2.建立队列
channel.queue_declare(queue='hello') # 队列名'hello'
# 3.发送消息
channel.basic_publish(exchange='',
routing_key='hello', # 往名为'hello'的队列中发送
body='123') # 消息内容
# 4. 关闭队列
connect.close()
注意:在RabbitMQ里消息并不能直接发送给队列,所有的信息发送都要通过一个exchange,但是这里我们先把这个exchange定义成一个空的字符串,后面再讲他的具体用法
- Customers.py
import pika
# 建立连接并定义好队列名
connect = pika.BlockingConnection(
pika.ConnectionParameters('localhost')
)
channel = connect.channel()
# 定义队列
channel.queue_declare(queue='hello')
# 定义回调函数,用来处理从队列中拿到的数据
def callback(ch,method,properties,body):
print("[x] Received %r"%body)
# 声明收消息的语法
channel.basic_consume(
'hello', # 队列名 queue='hello'
callback, # 回调函数
True, # no_ack=True
)
# 开始收消息
channel.start_consuming()
# 注意: start_consuming()方法启动后,会一直收消息,若没有消息这卡住
打印结果: [x] Received b'123'
疑问:我们不是在生产者里已经定义了队列名吗?为什么在消费者里还要定义呢?
因为在实际工作中,我们并不能确定是生产者还是消费者先一步运行,如果队列名没有定义的话运行时候是会报错的。下面就是对消息的处理
回调函数的参数:
- body: 从队列中拿到的消息内容,是bytes类型
- ch: conne.channel的内存对象地址
- method: 包含了发送的信息
<Basic.Deliver(
['consumer_tag=ctag1.9ae48c906b014a83a512413c0e6f9ef8',
'delivery_tag=1',
'exchange=',
'redelivered=False',
'routing_key=hello']
)>
公平分发(一对多)
在这种结构里,我们要考虑到这样一种情况:有多个消费者,消费者在得到消息时需要对消息进行处理,并且有可能处理消息所消耗的时间是不同的
轮询分发方式:
我们启动多个消费者Customer,然后依次启动多个生产者Producer,会发现,消息是公平的依次分发给每一个消费者,这就叫轮询分发
消息确认message acknowledgments :
考虑下这种情形:消费者在处理消息时需要较长的时间,在这时把这个消费者kill掉,正在处理的消息和已经接收但未被处理的消息就丢失了。这应该是不允许的,我们可不希望有数据丢失,就需要将这些任务重新发送给其他正常工作的消费者。
为了保证任务不丢失,RabbitMQ支持使用message acknowledgments,消费者在完成任务后会给RabbitMQ发送个消息,告诉他活已经干完了,RabbitMQ就会把这个任务给释放掉。而当出现消费者宕机、掉线等情况时,RabbitMQ会重新把这个任务发送给其他的消费者
- no_ack = True ,当消费者接收到消息后,RabbitMQ就直接销毁掉这个消息.
- no_ack = False ,当消费者接收到消息后,RabbitMQ不主动销毁掉这个消息,这样,当一个消费者宕机了,RabbitMQ就会直接把任务拍个下一个消费者。
注意:
# 定义回调函数
def callback(ch,method,properties,body):
print("[x] Received %r"%body)
# 用来告诉Rabbitmq,消费者已经成功处理了消息任务,队列中的这个消息任务可以删除了
ch.basic_ack(delivery_tag = method.delivery_tag)
channel.basic_consume(
'hello',
callback,
False # 当no_ack为False时,回调函数中要指定basic_ack方法
)
这里有一个问题: RabbitMQ是如何知道消费者kill了呢? 其实就是socket,消费者kill后socket就断开连接了,所以RabbitMQ就把任务排给下一个消费者
补充:
查看RabbitMQ有哪些队列,有多少消息?
进入Rabbitmq\rabbitmq_server-3.8.1\sbin文件夹下,执行rabbitmqctl.bat list_queue
消息持久化Message durability
如果RabbitMQ如果断掉(或者服务重启)了,里面的任务(包括所有queue和exchange依旧会丢失)这时候我们可以用到——消息持久化
- 队列持久化
channel.queue_declare(queue='hello',durable=True) # 将队列持久化(只保存了队列)
注意: 生产者和消费的这条代码都要这么写,才能保证在持久化队列的时候要保持生产者和消费者的一致性
- 消息持久化
channel.basic_publish(exchange='',
routing_key='hello',
body=message,
properties=pika.BasicProperties(delivery_mode=2) # 保持消息持久化
)
注意几点:
- 如果只持久化了消息,服务重启后消息丢失
- 如果只持久化了队列,服务重启后队列还在,但消息丢失
- 在持久化队列的时候要保持生产者和消费者的一致性
消费者任务负载平衡
因为有可能每个消费者处理信息的能力不一样,如果按公平分发的化有可能导致负载不平衡,旱的旱死、涝的涝死。为避免这种情况发生还有一个知识点
在消费者端加入以下代码,
channel.basic_qos(prefetch_count=1) # 每次只处理一个消息任务,处理完在接收下一个
最终代码演示:
- Producers.py
import pika
#1. 建立连接
connect = pika.BlockingConnection(
pika.ConnectionParameters('localhost')
)
# 建立管道
channel = connect.channel()
# 2.建立队列 并持久化
channel.queue_declare(queue='hello', durable=True) # 队列名'hello'
# 3.发送消息 并持久化消息
channel.basic_publish(exchange='',
routing_key='hello', # 往名为'hello'的队列中发送
body='123' # 消息内容
properties=pika.BasicProperties(delivery_mode=2)
)
# 4. 关闭队列
connect.close()
- Customers.py
import pika
# 建立连接并定义好队列名
connect = pika.BlockingConnection(
pika.ConnectionParameters('localhost')
)
channel = connect.channel()
# 定义队列 并持久化
channel.queue_declare(queue='hello', durable=True)
# 定义回调函数,用来处理从队列中拿到的数据
def callback(ch,method,properties,body):
print("[x] Received %r"%body)
ch.basic_ack(delivery_tag = method.delivery_tag) # 消息回执
channel.basic_qos(prefetch_count=1) #限制消费者待处理任务个数
# 声明收消息的语法
channel.basic_consume(
'hello', # 队列名 queue='hello'
callback, # 回调函数
False
)
# 开始收消息
channel.start_consuming()
发布/订阅(publish/subscribe)
我们在前面两部分将的都是将消息由生产者到消费者之间通过queue传递,现在将引入一个新的成员:exchange。
其实生产者在发送的时候是不知道消息要发送给那个queue的,甚至他都不知道消息是由queue接收的。实际上生产者只是把message发送给了exchange。至于message后续的处理都是由exchange决定的。
就像图上标示的,exchange在sender和queue之间起到了转呈的作用。
按照工作方式,我们将exchange分成了fanout、direct、topic和headers四种类型。
- fanout:所有绑定到这个exchange的队列都接收消息
- direct:通过routingKey和exchange决定的那个唯一的queue可以接收消息
- topic:所有符合routingKey(可以是表达式)的queue可以接收消息
表达式说明:#表示一个或多个字符
*表示任何字符
使用RoutingKey为#时相当于fanout
- headers:通过headers来决定把消息发送给哪些queue。
fanout的作用
生产者端:
在生产者端定义一个exchange,名字随便起一个‘logs’,类型就声明为fanout。
channel.exchange_declare(exchange='logs',
exchange_type='fanout')
注意: ,exchange=''空的字符串表示了默认的exchange或名字是空的,那exchange就把消息发送给routing_key指定的queue里(前提是这个queue是存在的),在声明了exchange以后,我们就可以用这个exchange发送消息了
import pika
connect = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connect.channel()
channel.exchange_declare(exchange='logs',
exchange_type='fanout') #logs 是随便起的名字,声明了exchange
message = 'info: Hello World!'
channel.basic_publish(exchange='logs', #使用的exchange名称
routing_key='', #使用的队列名称
body=message) #消息内容
这里并没有定义队列的名称?为什么?在广播的时候是不用固定具体的哪个queue的
消费者端:
result = channel.queue_declare() #生成随机queue
result = channel.queue_declare(exclusive=True)
这个exclusive表示在连接在关闭以后这个queue直接被销毁掉。
然后把这个queue绑定在转发器上。
所有进入这个exchange的消息被发送给所有和他绑定的队列里
随机的queue已经声明了,现在就把他跟exchange绑定
channel.queue_bind(exchange='logs',
queue=result.method.queue)
import pika
connect = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connect.channel()
channel.exchange_declare(exchange='logs',exchange_type='fanout')
result = channel.queue_declare(exclusive=True) #exclusive 唯一的,为True时不指定queue名的化随机生成一个queue,
# 在断开连接后把queue删除,相当于生成一个随机queue
channel.queue_bind(exchange='logs',
queue=result.method.queue) #绑定的是exchange对应的queue
print('waiting for logs.')
def callback(ch,method,preproteries,body):
print('get data:%r'%body)
channel.basic_consume(callback,
queue=result.method.queue,
no_ack=True)
channel.start_consuming()
总结:
发送端的代码跟前面的差不太多,最重要的差别就是把routingKey给忽略掉了,但是明确了exchange的对象。
而接收方是在建立连接后要声明exchange,并且要和队列绑定。如果没有队列和exchange绑定,消息就被销毁了。这就是整个发送的过程
队列和ex绑定,生产者将消息任务发给ex,ex便利与之绑定的队列,依次发送任务,消费之从队列中接收任务
订阅—发布的模型就像电台和收音机一样,如果customer下线了是收不到信息的,消息也是在线发送的,并不会保存。
direct的作用
在这个过程中,我们大致了解了发布——订阅模型。其实就是在发送端定义了一个exchange,在接收端定义了一个队列,然后把这两者绑定,就OK了。可是我们现在只想订阅一部分有用的信息,比如只获取错误信息写到日志文件里,但同时又能将所有的信息都显示在控制台(或者terminal上)。
上一节所讲述的bind,也可以简单的理解为这个queue对这个exchange的内容“感兴趣”。
在binding的时候,还可以加一个routingKey的参数,这个参数就描述了queue对哪些exchange感兴趣。
channel.queue_bind(exchange='logs', #被绑定的exchange名
queue='queue_name', #被绑定的queue名
routing_key='black') #queue的‘兴趣爱好’
对queue和exchange进行bind时,bind的参数主要取决于exchange的类型,比如在fanout模式下是不能有这个routingKey的,运行时候会报错。
我们使用了fanout的发布订阅模式,在这个模式下接收端不能对信息进行一定原则的过滤,一股脑的照单全收,已经不能满足我们的要求了,现在就要用direct模式。
在上面的图里,有两个queue分别和exchange绑定,Q1的routingKey是orange,Q2则有两个分别是black和green。在这个模型中,发布的消息关键字是orange则被分发到Q1内,而包含有black或green的则发给Q2.剩余的消息就被discard了。
而在上图中,同样的key同多个队列进行绑定的方法也是合法的。所有包含关键字black的消息会被同时发送 给Q1和Q2。
了解了上面所说的方法,我们来按照本节一开始的目标来修改下代码
生产者:
def producers_direct(msg): # "msg":{"type":"全量", "name":"xionger", "pwd":"123"}
t = msg.get('type')
m = msg.get('name')
msg = json.dumps(m)
# 1. 建立连接
connect = pika.BlockingConnection(
pika.ConnectionParameters(host='127.0.0.1')
)
# 2. 建立管道
channel = connect.channel()
channel.exchange_declare(exchange='task',
exchange_type='direct') # task 是随便起的名字,声明了exchange
serverity = t # 此处为 '全量' 即 下面的 路由键(routing_key) 为 '全量'
# 3.发送消息
channel.basic_publish(exchange='task',
routing_key=serverity, # 消息的分类
body=msg,
# properties=pika.BasicProperties(delivery_mode=2) # 消息持久化
)
# 4. 关闭队列
connect.close()
消费者:
import pika
import time
import json
connect = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connect.channel()
channel.exchange_declare(exchange='task',
exchange_type='direct',
)
result = channel.queue_declare('',exclusive=True) # 随机生成队列, 端口连接后队列自动删除
queue_name = result.method.queue
servrities = ['增量', '全量'] # 获取所有的关键字
channel.queue_bind(exchange='task',
queue=queue_name,
routing_key=servrities[0]) # 此处 路由键 为 '增量', 即只处理 '增量' 的任务
def callback(ch, method, preproteries, body):
time.sleep(5)
body = json.loads(body.decode('utf-8'))
print('Z %s: body %s' % (method.routing_key, body))
channel.basic_consume(queue_name, callback,True)
channel.start_consuming()
topic的作用
在上一节我们利用了direct的模式实现了初步的消息过滤,在这一节里,我们要看看如何实现如何实现更加细致的消息过滤
就想这个图里的一样,我们在定义RoutingKey的时候利用了表达式,就像模糊查询一样其中
*表示任意一个字符
#表示0个或多个字符
topic模式的代码和上一节的基本一致,只是改变了exchange的模式
channel.exchange_declare(exchange='logs',
exchange_type='topic')#声明exchange
生产者端:
connect = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connect.channel()
首先要声明exchange
channel.exchange_declare(exchange='logs',
exchange_type='topic') #声明exchange
在发送的时候对消息进行分类
routing_key = sys.argv[1] if len(sys.argv)>1 else 'info'
message = 'info: Hello World!'
然后发送消息
channel.basic_publish(exchange='logs',
routing_key=serverity, #消息的分类
body=message)
消费者端
import pika
connect = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connect.channel()
channel.exchange_declare(exchange='logs',
exchange_type='topic')
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
servrities = sys.argv[1:] #获取所有的关键字
if not servrities:
sys.stderr.write('Usage: %s [info] [warning] [error]\n'%sys.argv[0])
sys.exit(1) #关键字不存在打印提示后退出
print('recived:%s'%servrities)
for servrity in servrities: #循环绑定
channel.queue_bind(exchange='logs',
queue=queue_name,
routing_key=servrity)
def callback(ch,method,preproteries,body):
print('[x] %r:%r' %(method.routing_key, body))
channel.basic_consume(callback,
queue=queue_name,
no_ack=True)
channel.start_consuming()
PRC
我们在前面的章节将到了在多个消费者之间分发耗时任务的方法,可是现在要实现这样的功能:调用远程的设备上的一个函数,然后等执行完毕返回结果。这样的工作模式就叫远程过程调用——Remote Procedure Call(RPC)。
利用RabbitMQ也可以实现RPC的功能,为了能模拟这个过程,我们在server端设立一个fun:给定一个整数n,然后返回n对应的斐波那契数列。
callback queue
通过RabbitMQ实现RPC的方法很简单——客户端发送请求,服务端对请求响应然后把消息发送至叫callback的queue,过程类似这样
result = channel.queue_declare(exclusive=True)
callback_queue = result.method.queue
channel.basic_publish(exchange='',
routing_key='rpc_queue',
properties=pika.BasicProperties(
reply_to = callback_queue,
),
body=request)
Correlation id
我们刚才为每个请求对应的响应都声明了一个队列,但是在这里等待着结果的返回效率是不是太低了?还好有另外的一种方法:为每个客户端创建一个callback的队列。然而又引发了一个新问题:在这个队列里我不知道哪个响应是对应我这个请求的!这时候就到大神出马了——Correlation ID。对每个请求都设置一个唯一的ID,在callback的队列里通过查看属性来判断他对应哪个请求。如果出现没有对应的ID,安全起见我们还是把他忽略掉。
总之我们的RPC的工作流程就是这样的:
1.client启动,声明一个匿名的callback queue
2.建立RPC请求,请求里除了消息还包含两个参数:a.replay_to(告诉server响应的结论callback的队列里)
b.correlation_id:每个请求都被赋予一个独一无二的值
3.请求被发送给RPC_queue
4.server等待queue里的消息,一旦出现请求,server响应请求并把结论发送给通过replay_to要求的queue里
5.client在callback_queue里等待数据,一旦消息出现,他将correlation进行比对,如果相同就获取请求结果。
案例:
# 生产者端:
import pika
import uuid
class RpcClient(object):
# 初始化时是一个消费者
def __init__(self):
self.connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
self.channel = self.connection.channel()
# 随机生成队列
result = self.channel.queue_declare('', exclusive=True)
# self.callback_queue 即队列名
self.callback_queue = result.method.queue # 回调队列
# 由生成者转换成消费者
self.channel.basic_consume(self.callback_queue, self.callback, True,)
def callback(self, ch, method, props, body):
if self.corr_id == props.correlation_id:
self.response = body
# 调用call方法时,是一个生产者
def call(self, msg):
msg = json.dumps(msg) # 需要序列化
self.response = None
self.corr_id = str(uuid.uuid4())
self.channel.basic_publish(exchange='',
routing_key='rpc_queue',
properties=pika.BasicProperties(
reply_to=self.callback_queue, # 消费者端通过 props.reply_to 即可拿到这个随机生成的队列
correlation_id=self.corr_id, # 消费者端通过 props.correlation_id 即可拿到 corr_id
),
body=msg)
while self.response is None:
self.connection.process_data_events() # 是一个等待消息的阻塞过程,连接的任何消息都可以使它脱离阻塞状态
return self.response.decode('utf-8') # 此处返回的就是消费端出完任务后回馈的信息
# 调用
rpc = RpcClient() # 普通模式 + rpc
res = rpc.call(msg)
print(res)
#消费者端:
import pika
import time
import json
name = ['xiongda', 'xionger']
pwd = '123'
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='rpc_queue')
def task_fun(body, props):
data = json.loads(body.decode('utf-8')) # 需要解码 + 反序列化
if data.get('name') in name and data.get('pwd') == pwd:
time.sleep(5)
res_dict = {'status':True, 'task_id':props.correlation_id}
return res_dict
else:
res_dict = {'status': False, 'task_id': props.correlation_id}
time.sleep(5)
return res_dict
# 1.消费者在回调函数中 通过 fun 函数处理任务
# 2.消费者在回调函数中 转换身份,变成生产者,
def callback(ch, method, props, body):
response = task_fun(body, props)
# 由消费之转换成生产者
ch.basic_publish(exchange='',
routing_key=props.reply_to, # props.reply_to 就是生产者随机生成的 队列
# props.correlation_id 就是 生产者那么生成的 corr_id,
properties=pika.BasicProperties(correlation_id=props.correlation_id),
body=str(response))
ch.basic_ack(delivery_tag=method.delivery_tag) # 消息确认机制
channel.basic_qos(prefetch_count=1) # 负载均衡
channel.basic_consume('rpc_queue', callback, )
print(" [x] Awaiting RPC requests")
channel.start_consuming() # 等待任务
总结
一对一 或 一对多:
1.生产者与消费之通过同名队列进行绑定,exchange=''
2.可以做队列与消息的持久化
3.同一个管道的消费者可以启动多个
4.可以设置消费者的消费能力(负载均衡)
5.可以设置消息确认机制
发布与订阅:
生产者不需要指定队列名,但需要指定 exchange, 所有消费者队列通过该 exchange 与生产者对应
fanout:
1.消费者端的队列名一般是随机的
2.同一个队列名的消费者同一时刻只能启动一个(重复启动会 406 错误)
3.生产者端的 routing_key 为空
4.消费者端不需要 routing_key 参数
direct:
1.消费者端的队列名一般是随机的
2.同一个队列名的消费者同一时刻只能启动一个(重复启动会 406 错误)
3.生产者端的 routing_key 为指定的'类别'名
4.消费者端通过 routing_key 指定的 '类别'名,来获取指定'类别'的任务, 即队列是有'兴趣爱好'的