rabbitmq
rabbitmq:
MQ 消息队列,是一种应用程序和应用程序之间的通讯方法
应用程序通过读写队列,来进行相互沟通,而无需建立专门的连接
与python支持的原生队列不同,此时队列部署在一台独立的server上,通过服务的接口调用,来实现队列数据的读写
安装:
server
client:
pika
- 关于 declare:
- 不论是exchange还是queue,都要先创建才能使用
- declare 表示,若存在,则什么事情都不做;若不存在,则创建
- 生产者:
import pika# BlockingConnection创建一个连接对象,并指定连接主机, 为该连接对象创建连接通道connection = pika.BlockingConnection(pika.ConnectionParameters(host='115.182.193.153'))channel = connection.channel()# 声明一个队列,若不存在则创建channel.queue_declare(queue='mq1')# 发送消息message = '2 test'channel.basic_publish(exchange='', routing_key='mq1', body=message)print('send: {}'.format(message))
- 消费者:
# BlockingConnection创建一个连接对象,并指定连接主机, 为该连接对象创建连接通道connection = pika.BlockingConnection(pika.ConnectionParameters(host='115.182.193.153'))channel = connection.channel()# 声明一个队列,若不存在则创建channel.queue_declare('mq1')
# 回调函数(线程的任务), 必须定义如下参数,def callback(channel, method, properties, body ):channel.basic_ack(delivery_tag=method.delivery_tag)print('recived: {}'.format(body))
# 定义消费者线程所需执行的任务:会将多个参数传递给回调方法channel.basic_consume(callback, queue='mq1', no_ack=False)# 开始消费,会持续监听队列获取消息channel.start_consuming()
消息丢失:
- client端挂掉: 从队列中获取了一个数据,但是没有来得及处理就down机了(连接断开,等各种原因导致),则该消息丢失,即消息虽然被获取了,但是该消息并没有被处理
- 措施:
- no-ack=False
- 每次client获取消息后,需要发送ack给server,
- 在server没有收到ack之前,会保存一个“消息副本”在队列中;当收到ack后,删除该副本
- 主要在消费者端解决:
import pika import time import threading class rabbitmq: def __init__(self, mq_ip, mq_port): self.connection = pika.BlockingConnection(pika.ConnectionParameters(host=mq_ip, port=mq_port)) self.channel = self.connection.channel() def publish(self, msg, mq_queue): self.channel.queue_declare(queue=mq_queue) self.channel.basic_publish(exchange='', routing_key=mq_queue, body=msg) print('send msg: {}'.format(msg)) def call_back(self, channel, method, properties, body): print('recived: {}'.format(body.decode())) time.sleep(10) channel.basic_ack(delivery_tag=method.delivery_tag) # 立即打印获取到的消息,只是休息10秒后,才会给server回复ack;在此过程中,数据一致保留在server上
print('send ack)
def consume(self, mq_queue): self.channel.basic_consume(self.call_back, queue=mq_queue, no_ack=True) self.channel.start_consuming() def publish(): sender = rabbitmq('115.182.193.157', 5672) for i in range(10):
sender.publish('qiaogy', 'msg-03')def consu(): recver = rabbitmq('115.182.193.157', 5672) recver.consume('msg-03') consu()
注意:若server 的 no_ack=True ,表示 将消息扔给“消费者”就行,不用管他处理的情况若server 的 no_ack=False , 表示,扔给“消费者”的仅仅是消息的“副本”,每次client处理完数据,都要返回一个ack,server收到ack后会删除该消息
# 测试:# client端获取消息后,10s内;server上通过命令查看 :rabbitmqctl list_queues,消息数目没有改变# 只有当 client 打印了send ack to server (发送了ack),server上消息数目才会减少
- server端挂掉,则所有队列中数据丢失
- 措施:
- 发送的数据进行持久化
- 主要在生产者端解决:
import pika import time import threading class rabbitmq: def __init__(self, mq_ip, mq_port): self.connection = pika.BlockingConnection(pika.ConnectionParameters(host=mq_ip, port=mq_port)) self.channel = self.connection.channel() def publish(self, msg, mq_queue): self.channel.queue_declare(queue=mq_queue, durable=True) # 声明该队列开启持久化的功能 self.channel.basic_publish(exchange='', routing_key=mq_queue, body=msg, properties=pika.BasicProperties(delivery_mode=2)) # 发送数据时,声明该数据需要持久化 print('send msg: {}'.format(msg)) def call_back(self, channel, method, properties, body): print('recived: {}'.format(body.decode())) time.sleep(10) channel.basic_ack(delivery_tag=method.delivery_tag) # 给server回复ack def consume(self, mq_queue): self.channel.basic_consume(self.call_back, queue=mq_queue, no_ack=True) self.channel.start_consuming() def publish(): sender = rabbitmq('115.182.193.157', 5672) sender.publish('qiaogy', 'msg-03') def consu(): recver = rabbitmq('115.182.193.157', 5672) recver.consume('msg-03') for i in range(10): publish()
测试:
没有开启持久化前,向队列发送10条消息,重启队列消息丢失;开启后,重启队列,消息依然存在
公平分发机制:
- 若有N个消费者,那么rabbitmq会将第X个消息发给第n个消费者(n = X mod N)
- 解释:
- 若有2个消费者,那么队列中所有序号为“奇数”的消息会发送给“消费者1”;序号为“偶数”的消息会发送给“消费者2”
- 即:rabbitmq 会按照“消费者数目”将队列中任务进行划分,保证每个消费者处理的任务数目相当
- 问题
- 若有很多任务和很多消费者,且任务执行周期有长有短
- 会造成:
- 队列中还有很多剩余任务没有被处理,
- 有的消费者很快完成了自己的任务,且处于空闲状态;有的消费者却从头忙到尾一直停不下来
- 场景展现:
- 消费者A:
import pika import time import threading class rabbitmq: def __init__(self, mq_ip, mq_port): self.connection = pika.BlockingConnection(pika.ConnectionParameters(host=mq_ip, port=mq_port)) self.channel = self.connection.channel() def publish(self, msg, mq_queue): self.channel.queue_declare(queue=mq_queue, durable=True) self.channel.basic_publish(exchange='', routing_key=mq_queue, body=msg, properties=pika.BasicProperties(delivery_mode=2)) print('send msg: {}'.format(msg)) @staticmethod def call_back(channel, method, properties, body): cur_thread = threading.current_thread() print('i\'m {} ,recived: {}'.format(cur_thread,body.decode())) time.sleep(4) # 消费耗费时间,A和B分别为1秒和3秒 channel.basic_ack(delivery_tag=method.delivery_tag) def consume(self, mq_queue): self.channel.basic_consume(self.call_back, queue=mq_queue, no_ack=False) self.channel.basic_qos(prefetch_count=1) # 假若我消费不过来(很忙),那么请把后面的消息发送给别的消费者 self.channel.start_consuming() def publish(): sender = rabbitmq('115.182.193.157', 5672) for i in range(30): sender.publish(str(i)+'message', 'queue-1') def consu(): recver = rabbitmq('115.182.193.157', 5672) recver.consume('queue-1') 注意: 应该先启动2个消费者,再启动生产者,参数的意思是告诉 server端下发消息的策略 ####### 消费者 A consume() ####### 消费者 B consume() ####### 生产者 publish()
- 现象描述(没有qos参数情况下):
- 消费者A:很快全部接受了2 , 4, 6, 8
- 消费者B:处理很慢,每隔3秒接受一个数字,1,3......
- 相关参数:
- 本质:声明,“我还有剩余任务需要处理,且当前很忙,那么请把剩余的任务分摊给别人”
channle.basic_qos(prefetch_count=1)channle.basic_consume(callback, queue='qiao', no_ack=False)
补充:创建随机的队列
- result =channel.queue_declare(exclusive=True)
- queue_name =result.method.queue
发布订阅:
- exchange:
- redis中发布订阅的本质:向频道发送一次消息,订阅该频道的所有client都能接收到该消息
- 相当于rabbitmq的交换机,而 fanout 类型的交换机表示:透传,可让每个绑定的队列都收到发给exchange的消息
- 示例
- 创建一个exchange,并绑定多个队列
import pika import threading class rabbitmq: def __init__(self, mq_ip, mq_port): self.connection = pika.BlockingConnection(pika.ConnectionParameters(host=mq_ip, port=mq_port)) self.channel = self.connection.channel() def bind_ex(self, lst, ex_name): # 创建一个交换机,并让列表中的队列都绑定到该交换机上 self.channel.exchange_declare(exchange=ex_name, type='fanout') # 指定交换机类型为fanout,表示发送给所有绑定的mq for mq in lst: self.channel.queue_declare(queue=mq, durable=True) self.channel.queue_bind(exchange=ex_name, queue=mq) def publish(self, ex_name, msg): # 生产者生产消息,只需要指定exchange名称 self.channel.basic_publish(exchange=ex_name, routing_key='', body=msg, properties=pika.BasicProperties(delivery_mode=2)) print('send msg: {}'.format(msg)) @staticmethod def call_back(channel, method, properties, body): cur_thread = threading.current_thread() print('i\'m {} ,recived: {}'.format(cur_thread,body.decode())) channel.basic_ack(delivery_tag=method.delivery_tag) # 给server回复ack def consume(self, mq_queue): # 消费各自去自己的不同mq接受消息 self.channel.basic_consume(self.call_back, queue=mq_queue, no_ack=False) self.channel.basic_qos(prefetch_count=1) self.channel.start_consuming() # 生产者: # sender = rabbitmq('115.182.193.157', 5672) # sender.bind_ex(['qiao-1','qiao-2','qiao-3'], 'qiaogy_group') # for i in range(50): # sender.publish('qiaogy_group',str(i)+'msg') # 分别打开不同的client ,发现每个client都可以接受到所有消息 # consume_qiao1 = rabbitmq('115.182.193.157', 5672) # consume_qiao1.consume('qiao-1') # consume_qiao2 = rabbitmq('115.182.193.157', 5672) # consume_qiao2.consume('qiao-2') consume_qiao3 = rabbitmq('115.182.193.157', 5672) consume_qiao3.consume('qiao-3')
关键字匹配:
- exchange type = direct
- exchange 在绑定队列的时候,可以指定关键字;相同关键字的分为一组
- 发送消息时,可以携带关键字,即发送给指定的一组队列
- 初始化:
- 定义一个direct类型的exchange,将lst1中队列绑定,并指定关键字info;将lst2中队列绑定,并指定关键字error
#!/usr/bin/env python # -*- encoding:utf-8 -*- import pika import threading class Rabbitmq: def __init__(self, mq_ip, mq_port): self.connection = pika.BlockingConnection(pika.ConnectionParameters(host=mq_ip, port=mq_port)) self.channel = self.connection.channel() # 讲列表中的队列绑定给指定交换机(exchange),并指定分组名称(routing_key) def bind_ex(self, lst, ex_name, group_name): self.channel.exchange_declare(exchange=ex_name, type='direct') for mq in lst: self.channel.queue_declare(queue=mq, durable=True) self.channel.queue_bind(exchange=ex_name, queue=mq, routing_key=group_name) # 向指定交换机发送消息,并路由给指定分组 def publish(self, ex_name, group_name, msg): self.channel.basic_publish(exchange=ex_name, routing_key=group_name, body=msg, properties=pika.BasicProperties(delivery_mode=2)) print('send msg: {} to group {}'.format(msg, group_name)) @staticmethod def call_back(channel, method, properties, body): cur_thread = threading.current_thread() print('i\'m {} ,recived: {}'.format(cur_thread,body.decode())) channel.basic_ack(delivery_tag=method.delivery_tag) # 给server回复ack def consume(self, mq_queue): # 消费各自去自己的不同mq接受消息 print('{}'.format(mq_queue).center(40, '=')) self.channel.basic_consume(self.call_back, queue=mq_queue, no_ack=False) self.channel.basic_qos(prefetch_count=1) self.channel.start_consuming()
vim init.py
vim recver.pyfrom RabbitClass import Rabbitmq sender = Rabbitmq('115.182.193.157', 5672) lst0 = ['mq-01', 'mq-02', 'mq-03'] sender.bind_ex(lst0, 'sw', 'info') ## 讲routing_key 为info的3个队列绑定到名称为 sw的exchange 上 lst1 = ['mq-11', 'mq-12', 'mq-13'] sender.bind_ex(lst1, 'sw', 'waring') lst2 = ['mq-21', 'mq-22', 'mq-23'] sender.bind_ex(lst2, 'sw', 'error')
#!/usr/bin/env python # -*- encoding:utf-8 -*- import threading from RabbitClass import Rabbitmq Sender = Rabbitmq('115.182.193.157', 5672) mq_lst = ['mq-01','mq-02','mq-03','mq-11','mq-12','mq-13','mq-21','mq-22','mq-23'] def work(msg_queue): Sender.consume(msg_queue) for mq in mq_lst: t = threading.Thread(target=work, args=(mq,)) t.start()
# N个线程分别从N个队列中接收消息vim sender.py
#!/usr/bin/env python # -*- encoding:utf-8 -*- from RabbitClass import Rabbitmq
## 往名称为info的routing_key 所对应的队列发送消息 Sender = Rabbitmq('115.182.193.157', 5672) # for i in range(10): # Sender.publish('sw', 'info', str(i)+'info')
## 往名称为error的routing_key 所对应的队列发送消息 for i in range(10): Sender.publish('sw', 'error', str(i)+'error')
## 往名称为waring的routing_key 所对应的队列发送消息 # for i in range(10): # Sender.publish('sw', 'waring', str(i) + 'waring')
分别运行不同组的for循环,发送到不同的roiuting_key,可以看到recver.py 能从指定routing_key所对应的队列中接收到消息
关键字模糊匹配:
在topic类型下,可以让队列绑定几个模糊的关键字,
发送者将数据发送到exchange,exchange将传入的”路由值“和 ”关键字“进行匹配,
匹配成功,则将数据发送到指定队列。
发送者路由值 队列中
old.boy.python old.* -- 不匹配
old.boy.python old.# -- 匹配
* 只匹配一个单词
# 匹配0个或多个单词
此时:exchange type = topic
消费者:
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
# 创建exchange,指定类型;
channel.exchange_declare(exchange='topic_logs',type='topic')
# 创建随机队列
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
# 获取关键字
binding_keys = sys.argv[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))
channel.basic_consume(callback,queue=queue_name,no_ack=True)
channel.start_consuming()
生产者:
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
# 指定发送给的exchange
channel.exchange_declare(exchange='topic_logs',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()
补充:
各种app都用到了队列,比如saltstack,
saltstack-server:
agent1 订阅一个队列 1
agent2 订阅一个队列 2
agent3 订阅一个队列 3
# 多个类型的主机绑定到同一个exchange
server:
1、saltstack获取用户输入的命令 command,并随即生成一个队列 temp_q,组成元组(command, temp_q)
2、讲元组发送给exchange,由exchange负责分发给各个主机的队列
agent:
1、每个agent从各自的队列中获取元组
2、执行command,并将执行结果发送到temp_q
最终,server端东temp_q中获取各个主机的执行结果;结果获取完成后,将该temp_q 删除