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)
 



补充:创建随机的队列
  1. result =channel.queue_declare(exclusive=True)  
  2. 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
from 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')
vim recver.py

#!/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 删除



















 

posted on 2017-01-18 10:04  台灯不太亮  阅读(263)  评论(0编辑  收藏  举报

导航