python消息队列RabbitMQ
tags:
- python3
- python基础
categories:
- RabbitMQ
- 消息队列
- RPC实现
文章目录
一、消息队列RabbitMQ介绍
- 消息队列提出:我们知道在Python中有
- 线程QUEUE:threading QUEUE 同一进程下线程之间数据的交互
- 进程QUEUE:父进程与子进程进行交互,或者同属于同一父进程下多个子进程进行交互
- 如果两个独立的进程(可以是不同语言写的)想要交互通信,或者两台机器之间想要进行通信。
消息队列有RabbitMQ(比较火),ZeroMQ,ActiveMQ … - RabbitMq 是实现了高级消息队列协议(AMQP)的开源消息代理中间件。消息队列是一种应用程序对应用程序的通行方式,应用程序通过写消息,将消息传递于队列,由另一应用程序读取完成通信。而作为中间件的 RabbitMq 无疑是目前最流行的消息队列之一。
- RabbitMq 应用场景广泛:
- 系统的高可用:日常生活当中各种商城秒杀,高流量,高并发的场景。当服务器接收到如此大量请求处理业务时,有宕机的风险。某些业务可能极其复杂,但这部分不是高时效性,不需要立即反馈给用户,我们可以将这部分处理请求抛给队列,让程序后置去处理,减轻服务器在高并发场景下的压力。
- 分布式系统,集成系统,子系统之间的对接,以及架构设计中常常需要考虑消息队列的应用。
1. 1 RabbitMQ安装
- erlang 语言开发的。所以安装RabbitMQ之前先安装erlang
- 到erlang官网: https://www.erlang.org/downloads 下载 OTP 22.2 Windows 64-bit Binary File
- 安装配置erlang:加入环境变量D:\Erlang\bin
- 输入cmd,再输入erl,看到版本号就说明erlang安装成功了。
- erl和RabbitMQ版本对应关系:https://www.rabbitmq.com/which-erlang.html
- RabbitMQ官网:https://www.rabbitmq.com/
- 安装好之后。进入安装目录D:\RabbitMQ\rabbitmq_server-3.7.24\sbin
- 运行命令:rabbitmq-plugins enable rabbitmq_management
- 然后打开浏览器,输入localhost:15672 输入默认账号和密码guest和guest。登录成功
rabbitmqct1.bat list_queues #查看队列
1.2 RabbitMQ基本使用
- python连接RabbitMQ的模块有:pika, Celery,Haigha(官网给出)
- RabbitMQ也支持其他种类的语言。(官网可查)
- 安装python模块pika: pip install pika
- 官方教程网址:https://www.rabbitmq.com/getstarted.html
- 实现最简单的队列通信
#product.py服务器生产者
import pika
'''
连接参数 host,port(默认5672),virtual_host,credentials(验证)
credentials = pika.PlainCredentials('shampoo', '123456') # mq用户名和密码验证
虚拟队列需要指定参数 virtual_host,如果是默认的可以不填。
'''
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
# 声明queue(给队列起名)
channel.queue_declare(queue='hello')
# n RabbitMQ a message can never be sent directly to the queue, it always needs to go through an exchange.
# 基本发送
channel.basic_publish(exchange='', # 图中的X,不用深究。知道默认为''就行
routing_key='hello', # queue名字
body='Hello World!') # 发送的内容
print("消息发送完毕")
connection.close()
# consumer.py 客户端消费者
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
# 这里在声明一次hello的原因。如果消费者先运行,hello生产者还不存在就会报错。
# 如果确定服务器端先启动,队列名确定叫hello。下面也可以不写(推荐写上呀!!!兄弟)
channel.queue_declare(queue='hello')
def callback(ch, method, properties, body):
'''
:param ch: 管道的内存对象
:param method:
:param properties:
:param body:
:return:
'''
import time
print("开始")
time.sleep(10)
print("Received %r" % body)
ch.basic_ack(delivery_tag=method.delivery_tag)
# 消费消息,如果收到消息就调用callback处理消息。从hello队列中收消息。
# auto_ack 自动确认,消息处理完给服务器发消息
channel.basic_consume('hello', callback, True)
print('等待接收消息,关闭按CTRL+C')
channel.start_consuming()
1.3 RabbitMq 常用设置
- 远程连接rabbitmq server的话,需要配置权限
- 首先在rabbitmq server上创建一个用户
- 同时还要配置权限,允许从外面访问
# 创建账号
rabbitmqctl add_user admin 123456
# 设置用户角色
rabbitmqctl set_user_tags admin administrator
# 设置用户权限
rabbitmqctl set_permissions -p "/“oldlu” .""."".*"
# 设置完成后可以查看当前用户和角色(需要开启服务)
rabbitmqctl list_users
- 轮询处理消息。假如开启多个消费者,生产者发送消息后,先发送到先启动的消费者以此循环。
- 如果消费者处理消息时宕机啦。消费者(consumer)调用callback函数时,会存在处理消息失败的风险,如果处理失败,则消息丢失。但是也可以选择消费者处理失败时,将消息回退给 rabbitmq ,重新再被消费者消费,这个时候需要设置确认标识。
- auto_ack 自动确认False情况(默认) channel.basic_consume(‘hello’, callback, True)
- ch.basic_ack(delivery_tag=method.delivery_tag) 手动确认。如果不确认配置了False,任务只要有客户端它会一直重复做下去。
- 如果服务器生产者宕机的话,如果没有持久化,那肯定全都不见了。
- 如果Rabbit只管按顺序把消息发到各个消费者身上,不考虑消费者负载的话,很可能出现,一个机器配置不高的消费者那里堆积了很多消息处理不完,同时配置高的消费者却一直很轻松。为解决此问题,可以在各个消费者端,配置perfetch=1,意思就是告诉RabbitMQ在我这个消费者当前消息还没处理完的时候就不要再给我发新消息了。解决方法:客户端加入下面一句
# 客户端最多存一条
channel.basic_qos(prefetch_count=1)
二、RabbitMq 持久化
MQ默认建立的是临时 queue 和 exchange,如果不声明持久化,一旦 rabbitmq 挂掉,queue、exchange 将会全部丢失。所以我们一般在创建 queue 或者 exchange 的时候会声明持久化。客户端和服务器端都需要设置。
- queue 队列持久化
# 声明消息队列,消息将在这个队列传递,如不存在,则创建。durable=True 代表消息队列持久化存储,False 非持久化存储
result = channel.queue_declare(queue='python-test',durable=True)
- exchange 声明持久化
# 声明exchange,由exchange指定消息在哪个队列传递,如不存在,则创建.durable = True 代表exchange持久化存储,False 非持久化存储
channel.exchange_declare(exchange='python-test', durable=True)
注意:如果已存在一个非持久化的 queue 或 exchange ,执行上述代码会报错,因为当前状态不能更改 queue 或 exchange 存储属性,需要删除重建。如果 queue 和 exchange 中一个声明了持久化,另一个没有声明持久化,则不允许绑定。
- 消息持久化
虽然 exchange 和 queue 都申明了持久化,但如果消息只存在内存里,rabbitmq 重启后,内存里的东西还是会丢失。所以必须声明消息也是持久化,从内存转存到硬盘。
# 向队列插入数值 routing_key是队列名。delivery_mode = 2 声明消息在队列中持久化,delivery_mod = 1 消息非持久化
channel.basic_publish(exchange = '',routing_key = 'python-test',body = message,properties=pika.BasicProperties(delivery_mode=2))
三、RabbitMq广播模式
- 之前的例子都基本都是1对1的消息发送和接收,即消息只能发送到指定的queue里,但有些时候你想让你的消息被所有的Queue收到,类似广播的效果,这时候就要用到exchange了。
- 发布订阅和简单的消息队列区别在于,发布订阅会将消息发送给所有的订阅者,而消息队列中的数据被消费一次便消失。所以,RabbitMQ实现发布和订阅时,会为每一个订阅者创建一个队列,而发布者发布消息时,会将消息放置在所有相关队列中。
- Exchange在定义的时候是有类型的,以决定到底是哪些Queue符合条件,可以接收消息
- fanout: 所有bind到此exchange的queue都可以接收消息
- direct: 通过routingKey和exchange决定的那个唯一的queue可以接收消息
- topic:所有符合routingKey(此时可以是一个表达式)的routingKey所bind的queue可以接收消息
3.1 fanout模式
- 消息是实时的。 fanout类似收音机,服务器端发送时,客户端已经启动可以收到。客户端没启动它不会给你存着,就没收到之前信息。
# fanout_publisher.py
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare('logs', 'fanout')
#message = ' '.join(sys.argv[1:]) or "info: Hello World!"
message = "info: Hello World!"
# 广播模式下queue不用写。
channel.basic_publish(exchange='logs',
routing_key='',
body=message)
print(" [x] Sent %r" % message)
connection.close()
# _*_coding:utf-8_*_
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs', exchange_type='fanout')
# 不指定queue名字,rabbit会随机分配一个名字,exclusive=True会在使用此queue的消费者断开后,自动将queue删除
result = channel.queue_declare('', exclusive=True)
# 上面随机生成的queue
queue_name = result.method.queue
# queue 绑定到logs转发器上, 通过自动生成的queue连接转发器
channel.queue_bind(exchange='logs',
queue=queue_name)
print(' [*] Waiting for logs. To exit press CTRL+C')
def callback(ch, method, properties, body):
print(" [x] %r" % body)
channel.basic_consume(queue_name, callback, True)
channel.start_consuming()
3.2 direct 模式
- 有选择的接收消息(exchange type=direct)RabbitMQ还支持根据关键字发送,即:队列绑定关键字,发送者将数据根据关键字发送到消息exchange,exchange根据 关键字 判定应该将数据发送至指定队列。
- 和fanout一样消息是实时的。
- python direct_consumer.py error warning (输入接收的消息类型)
# direct_publisher
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs',
exchange_type='direct')
severity = sys.argv[1] if len(sys.argv) > 1 else 'info'
message = ' '.join(sys.argv[2:]) or 'Hello World!'
channel.basic_publish(exchange='direct_logs',
routing_key=severity,
body=message)
print(" [x] Sent %r:%r" % (severity, message))
connection.close()
# direct_consumer
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs',
exchange_type='direct')
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
# 获取输入的级别列表
severities = sys.argv[1:]
if not severities:
sys.stderr.write("Usage: %s [info] [warning] [error]\n" % sys.argv[0])
sys.exit(1)
# 所有发到severity参数的消息都收,其他的consumer中没有这个参数
for severity in severities:
channel.queue_bind(exchange='direct_logs',
queue=queue_name,
routing_key=severity)
print(' [*] Waiting for logs. To exit press CTRL+C')
def callback(ch, method, properties, body):
print(" [x] %r:%r" % (method.routing_key, body))
channel.basic_consume(queue_name, callback, True)
channel.start_consuming()
3.3 更细致的消息过滤topic
匹配关键字发送.info,比如消息后缀名为.info。相对于direct写死更加灵活。
python topic_consumer.py *.info
python topic_consumer.py *.error mysql.*
python topic_publisher.py test.error
# topic_publisher
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='topic_logs',
exchange_type='topic')
routing_key = sys.argv[1] if len(sys.argv) > 1 else 'anonymous.info'
message = ' '.join(sys.argv[2:]) or 'Hello World!'
channel.basic_publish(exchange='topic_logs',
routing_key=routing_key,
body=message)
print(" [x] Sent %r:%r" % (routing_key, message))
connection.close()
# topic_consumer.py
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='topic_logs',
exchange_type='topic')
result = channel.queue_declare('', exclusive=True)
queue_name = result.method.queue
binding_keys = sys.argv[1:]
if not binding_keys:
sys.stderr.write("Usage: %s [binding_key]...\n" % sys.argv[0])
sys.exit(1)
for binding_key in binding_keys:
channel.queue_bind(exchange='topic_logs',
queue=queue_name,
routing_key=binding_key)
print(' [*] Waiting for logs. To exit press CTRL+C')
def callback(ch, method, properties, body):
print(" [x] %r:%r" % (method.routing_key, body))
四、RabbitMQ的rpc实现
- RPC全称为Remote Procedure Call,意为远程过程调用。
- 假设有两台服务器A,B.A服务器上部署着一个应用a,B服务器上部署着一个应用b,现在a希望能够调用b应用的某个函数(方法),但是二者不在同一个进程内,不能直接调用,就需要通过网络传输,在AB服务器之间建一条网络传输通道,a把参数传过去,b接收到参数调用自己的方法得到结果,再通过网络传回给a。
- 简单讲就是A通过网络来调用B的过程,这个过程要涉及的东西很多,比如多线程、Socket、序列化反序列化、网络I/O,很复杂。于是牛掰的程序员把这些封装起来做成一套框架供大家使用,就是RPC框架。比如:经典的Thrift框架
- 先运行客户端, 在运行服务端。
# rpc_client
import pika, time
import uuid
class FibonacciRpcClient(object):
def __init__(self):
self.connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
self.channel = self.connection.channel()
# 自动创建一个queue
result = self.channel.queue_declare('', exclusive=True)
print(result.method.queue)
self.callback_queue = result.method.queue
# 只要收到服务器消息 就调用on_response函数
self.channel.basic_consume(self.callback_queue, self.on_response, True)
def on_response(self, ch, method, props, body):
# 确定服务器回消息的准确 哪个客户段给的id就返回哪个客户端
if self.corr_id == props.correlation_id:
self.response = body
def call(self, n):
self.response = None
self.corr_id = str(uuid.uuid4())
# 发的时候带着服务端需要回给的 队列名callback_queue 和 生产的唯一字符串corr_id
self.channel.basic_publish(exchange='',
routing_key='rpc_queue',
properties=pika.BasicProperties(
reply_to=self.callback_queue,
correlation_id=self.corr_id,
),
body=str(n))
# 一直等待直到初始化的basic_consume被执行。就代表服务器回消息啦
while self.response is None:
# 可以理解为非阻塞版的start_consuming 过一段时间检测一下
self.connection.process_data_events()
# 这里可以继续发消息不用等待 前面uuid可以保证返回消息返回对应问题
print("检测一下,有消息吗?")
time.sleep(0.5)
return int(self.response)
fibonacci_rpc = FibonacciRpcClient()
print(" [x] Requesting fib(30)")
# 客户段发命令
response = fibonacci_rpc.call(30)
print(" [.] Got %r" % response)
# rpc_server
import pika
import time
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='rpc_queue')
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n - 1) + fib(n - 2)
def on_request(ch, method, props, body):
n = int(body)
print(" [.] fib(%s)" % n)
response = fib(n)
ch.basic_publish(exchange='',
routing_key=props.reply_to,
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', on_request)
print(" [x] Awaiting RPC requests")
channel.start_consuming()