rabbitmq 再探

rabbitmq 再探

速食版

  • 多个 worker 订阅同一个 队列, 轮流从队列中获取消息
  • 将队列声明为持久化队列: channel.queue_declare(queue='hello', durable=True)
  • 若队列已经声明, 则修改参数再次声明 不会报错, 且不会修改队列参数
  • 将消息标记为持久化消息
    channel.basic_publish(exchange='',
                        routing_key="task_queue",
                        body=message,
                        properties=pika.BasicProperties(
                           delivery_mode = pika.spec.PERSISTENT_DELIVERY_MODE
                        ))
    

    PS: 即使这样写, 也只是告诉 rabbitmq 将消息写入磁盘, 但是这并不代表会确认是否写入磁盘,所以也有一定风险会丢失数据。

  • 通过设置 worker预约任务数量 来防止多个耗时时间长的任务分配到同一个 worker 中:
    channel.basic_qos(prefetch_count=1)
    
  • 消息默认是需要确认的, 即 auto_ack 选项默认为 False
  • RabbitMq 无法释放 未确认 的消息

多个订阅

productor

  • send.py 改为 productor.py, (productor 意思是 生产者, 表示生产消息的程序), 并将内容成下面这样
    #!/usr/bin/python3
    from time import sleep
    import pika
    import random
    
    # 初始化链接
    # PS: 因为我这里是在 docker for mac 环境中运行的, host.docker.internal 代表本机
    # PS2: 如果不理解上一句, 则无关紧要, 将下面的 host.docker.internal 替换为 localhost 即可
    connection = pika.BlockingConnection(pika.ConnectionParameters('host.docker.internal'))
    
    # 获取 channel, 应该是频道的意思
    channel = connection.channel()
    
    # 声明一个队列, declare 意思是声明
    channel.queue_declare(queue='hello')
    
    while True:
        # 定义一个 长度为 1~9 的 变量 length
        length = random.randint(1, 9)
        
        # 根据变量 length 生成一个 长度等于 length 的消息, 放入 msg 中
        msg = "".join(map(lambda x: str(random.randint(0,9)), range(0, length)))
    
        # 将 msg 推送到 名为 "hell" 的 队列
        channel.basic_publish(exchange='',
                            routing_key='hello',
                            body=msg)
        # 输出内容, 一般 [ ] xxx, 表示 xxx 未完成, 而 [x] xxx 则表示 xxx 已完成
        # 这里输出该内容表示 已经将 msg 发送。
        print(f" [x] Sent '{msg}'")
        
        # 让程序睡眠 length 秒
        sleep(length * .5)
    
    # 关闭连接
    connection.close()
    
  • 调整之后的脚本, 会不停生成消息到 hello 中

consumer

  • receive.py 改名为 consumer.py (PS: consumer 意思是 消费者, 表示 消费消息的程序), 并将内容调整为下面这样, 相比于之前的脚本, 本次修改增加了 睡眠机制, 根据消息的长度进行睡眠, 是为模仿真实的场景
    #!/usr/bin/python3
    from time import sleep
    import pika, sys, os
    
    def main():
        # 这里写为 host.docker.internal 的原因查看 send.py
        connection = pika.BlockingConnection(pika.ConnectionParameters(host='host.docker.internal'))
        channel = connection.channel()
    
        # 声明一个名为 "hello" 的 队列
        channel.queue_declare(queue='hello')
    
        # 队列回调函数, 将 接收到的消息 输出到控制台
        def callback(ch, method, properties, body):
            print(" [x] Received %r" % body.decode())
            sleep(len(body.decode()))
            print(" [x] Done")
    
        # 为队列配置监听方式, 即: 当队列中有数据的时候, 调用回调函数
        channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=True)
    
        # 输出信息, 告诉调用者使用 "ctrl+c" 快捷键停止监听
        print(' [*] Waiting for messages. To exit press CTRL+C')
    
        # 开始监听队列
        channel.start_consuming()
    
    if __name__ == '__main__':
        """启动 main 函数
        
        当按下 ctrl+c 后停止运行
        """
        try:
            main()
        except KeyboardInterrupt:
            print('Interrupted')
            try:
                sys.exit(0)
            except SystemExit:
                os._exit(0)
    

分发模式

  • 此时, 我们可以启动 一个 productor.py, 两个 consumer.py 来进行验证

  • 因为 productor.py 每秒生成个字符, 一个 consumer.py 每秒消费一个字符, 理论上, 生产与消费会达到平衡

  • 然而实际上会发现, 生产与消费并没达到平衡, 其原因如下:

    By default, RabbitMQ will send each message to the next consumer, in sequence. On average every consumer will get the same number of messages. This way of distributing messages is called round-robin. Try this out with three or more workers.

  • 看不懂? 没关系, 翻译如下:

    默认情况下,RabbitMQ 会按顺序将每条消息发送给下一个消费者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式称为循环。与三个或更多工人一起尝试一下。

  • 所以说, 可能会出现 大量耗时长的任务会分配给同一个 consumer.py

  • 根据官方文档所说, 使用 channel.basic_qos(prefetch_count=1) 即可以 避免此情况, 该设置调整了分配方式, 使得不会预先将任务分配给 consumer.py

消息确认

  • 我们之前的场景, 有这么一个隐患: 消息领取之后但是程序异常退出, 会导致消息丢失。
  • 那么该如何解决该隐患呢?
  • 此文档中给出了答案, 需要从两个方面解决

消息确认

  • 我们需要 consumer.py 一旦中途退出, 此消息还可以被其他 consumer.py 获取到
  • 这个场景, 用到的就是 消息确认
  • 消息确认的场景大概如下
    sequenceDiagram participant RabbitMq服务端 participant 消费者张三 participant 消费者李四 消费者张三->>RabbitMq服务端: 来个消息 RabbitMq服务端-->>消费者张三: 给你个"任务一" 消费者张三->>RabbitMq服务端: "任务一"我干完了(消息确认) 消费者张三->>RabbitMq服务端: 再来一个 RabbitMq服务端->>消费者张三: 给你个"任务二" RabbitMq服务端->>RabbitMq服务端: 等了一会儿 消费者李四->>RabbitMq服务端: 给我来一个 RabbitMq服务端->>消费者李四: 张三好像死了,你先干"任务二"把 消费者李四->>RabbitMq服务端: "任务二"我干完了(消息确认)
  • 默认是需要 消息确认 的, 只不过在之前的代码中将其设置为了 自动确认:
  • 即: consumer.pychannel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=True)auto_ack=True
  • 此时我们需要将 consumer.py 调整为下面这样
    #!/usr/bin/python3
    from time import sleep
    import pika, sys, os
    
    def main():
        # 这里写为 host.docker.internal 的原因查看 send.py
        connection = pika.BlockingConnection(pika.ConnectionParameters(host='host.docker.internal'))
        channel = connection.channel()
        
        # 将 预分配数量改为1
        channel.basic_qos(prefetch_count=1)
    
        # 声明一个名为 "task_queue" 的 队列
        channel.queue_declare(queue='task_queue')
    
        # 队列回调函数, 将 接收到的消息 输出到控制台
        def callback(ch, method, properties, body):
            print(" [x] Received %r" % body.decode())
            sleep(len(body.decode()))
            print(" [x] Done")
            ch.basic_ack(delivery_tag=method.delivery_tag)
    
        # 为队列配置监听方式, 即: 当队列中有数据的时候, 调用回调函数
        channel.basic_consume(queue='task_queue', on_message_callback=callback)
        # 输出信息, 告诉调用者使用 "ctrl+c" 快捷键停止监听
        print(' [*] Waiting for messages. To exit press CTRL+C')
    
        # 开始监听队列
        channel.start_consuming()
    
    if __name__ == '__main__':
        """启动 main 函数
        
        当按下 ctrl+c 后停止运行
        """
        try:
            main()
        except KeyboardInterrupt:
            print('Interrupted')
            try:
                sys.exit(0)
            except SystemExit:
                os._exit(0)
    
  • 上面的代码做了三个改动
    1. 将 队列名 hello 改为了 task_queue
    2. 关闭自动确认, 即: 将 auto_ack=True 删掉
    3. 当任务执行完毕后, 确认消息, 即: callback 中的 ch.basic_ack(delivery_tag=method.delivery_tag)

持久化消息

  • 首先, 从官方文档中得知, 消息 默认是不会写入到磁盘的, 要实现 将消息写入磁盘, 需要在声明队列发送消息 的时候, 调整配置

  • 声明队列(productor.py)

    channel.queue_declare(queue='hello', durable=True)
    

    其中 durable=True 表示将队列声明为持久化队列

    这里需要注意的是, 我们之前已经使用 channel.queue_declare(queue='hello') 命令声明过一次队列

    此时再使用 channel.queue_declare(queue='hello', durable=True) 声明队列不会报错, 但是也不会将当前队列修改为 持久化队列

    官方给的解决方法简单粗暴 且不优雅: 声明一个不同名的队列
    channel.queue_declare(queue='task_queue', durable=True)

  • 发送消息(productor.py)

    channel.basic_publish(exchange='',
                          routing_key="task_queue",
                          body=message,
                          properties=pika.BasicProperties(
                            delivery_mode = pika.spec.PERSISTENT_DELIVERY_MODE
                          ))
    

    其中 delivery_mode = pika.spec.PERSISTENT_DELIVERY_MODE 表示将消息声明为 持久化消息

posted @ 2022-04-12 12:28  R&D袁智远  阅读(53)  评论(0编辑  收藏  举报