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.py
中channel.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)
- 上面的代码做了三个改动
- 将 队列名
hello
改为了task_queue
- 关闭自动确认, 即: 将
auto_ack=True
删掉 - 当任务执行完毕后, 确认消息, 即:
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
表示将消息声明为 持久化消息