RabbitMQ 三种模式入门:HelloWorld、WorkQueue、Pub/Sub
写给刚学完 RabbitMQ 基本概念、还是不知道"消息队列到底怎么用"的同学。
写在前面
如果你看了一堆 RabbitMQ 教程,还是分不清 exchange / queue / routing_key,不知道什么时候该用哪种模式,那这篇文章就是为你写的。
我会用 生活化的类比 + ASCII 图解 + 实际能跑的代码 ,把官方教程前 3 种最基础的工作模式讲清楚。所有代码都配套在 01_helloworld/、02_workqueue/、03_pubsub/ 三个目录里,复制即用。
0. 前置知识
什么是消息队列?
想象你去快递站寄快递:
- 你(生产者)把包裹交给柜台
- 快递员(消费者)从柜台取走包裹去派送
- 柜台帮你解耦了"寄"和"送"两件事,你不用等快递员
RabbitMQ 就是这样一个"数字快递柜":
| 术语 | 别名 | 作用 |
|---|---|---|
| Producer | 生产者 | 往 RabbitMQ 发消息的程序 |
| Consumer | 消费者 | 从 RabbitMQ 收消息并处理的程序 |
| Queue | 队列 | 消息暂存的地方(先进先出) |
| Exchange | 交换机 | 决定消息该进哪个队列(Pub/Sub 起作用) |
| Routing Key | 路由键 | 消息的"标签",交换机据此决定路由 |
环境准备
- RabbitMQ 服务端(本地装或云服务,本文用
192.168.121.128:5672,账号admin/123456) - Python 3.x + pika 库
- 一个能连上服务端的环境
1. HelloWorld:最简单的一对一
1.1 场景
老师给小明发一条通知:"记得交作业"。
小明收到后回一句"好的",整个流程结束。
对应到 RabbitMQ:
- 1 个生产者发 1 条消息
- 1 个消费者收 1 条消息
- 收完就退出
1.2 图解
Producer RabbitMQ Consumer
(老师) (小明)
| | |
|--- 发 "交作业" ----->| |
| |-- "交作业" ------->|
| | |
| |<-- ack (收到了) ---|
- 用到的部件:1 个队列 (叫
hello) - 关键动作:消费者收到后回
basic_ack告诉服务端"我处理完了" - 收完 1 条就
stop_consuming()退出
1.3 代码
完整代码在 01_helloworld/。
producer.py(老师端):
python
import pika
credentials = pika.PlainCredentials('admin', '123456')
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='192.168.121.128', port=5672, credentials=credentials)
)
channel = connection.channel()
# 声明队列(不存在则自动创建)
channel.queue_declare(queue='hello', durable=True, exclusive=False, auto_delete=False)
# 发消息
channel.basic_publish(
exchange='', # 空字符串 = 默认交换机
routing_key='hello', # 默认交换机直接把消息丢到同名队列
body='记得交作业',
properties=pika.BasicProperties(delivery_mode=2), # 消息持久化
)
print(" [x] 已发送: 记得交作业")
connection.close()
consumer.py(小明端):
python
import pika
credentials = pika.PlainCredentials('admin', '123456')
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='192.168.121.128', port=5672, credentials=credentials)
)
channel = connection.channel()
channel.queue_declare(queue='hello', durable=True, exclusive=False, auto_delete=False)
def callback(ch, method, properties, body):
print(f" [x] 收到: {body.decode('utf-8')}")
ch.basic_ack(delivery_tag=method.delivery_tag)
ch.stop_consuming() # 收到 1 条就退出
channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=False)
print(" [*] 等待消息中...")
channel.start_consuming()
1.4 动手跑
bash
# 终端 A
.venv/Scripts/python.exe 01_helloworld/consumer.py
# 终端 B
.venv/Scripts/python.exe 01_helloworld/producer.py
终端 A 立刻打印 记得交作业 然后退出。
1.5 小结
| 关键点 | 说明 |
|---|---|
exchange='' |
用默认交换机 |
routing_key='队列名' |
默认交换机会把消息直接送到同名队列 |
durable=True |
队列持久化,RabbitMQ 重启不丢 |
delivery_mode=2 |
消息持久化 |
basic_ack |
消费者告诉服务端"我处理完了" |
auto_ack=False |
必须手动 ack,否则消息会一直堆积在队列里 |
2. WorkQueue:多工人抢任务
2.1 场景
公司有个订单系统,每天有几千个订单要处理。
- 1 个生产订单的"前台"(生产者)
- N 个处理订单的"客服"(消费者)
每来一个订单,只让一个客服处理 (同一订单不能被两个客服同时处理)。
哪个客服手头没事,就抢下一个订单。
这就是 WorkQueue(任务队列)模式的核心:1 个生产者派发,多个消费者竞争消费,每条消息只被处理一次。
2.2 图解
Producer Queue Consumer-1 Consumer-2
(前台) task_queue (客服A) (客服B)
| | | |
|-- 订单1 -------->| | |
|-- 订单2 -------->|-- 订单1 -------->| |
|-- 订单3 -------->|-- 订单2 --------------------->|
|-- 订单4 -------->|-- 订单3 -------->| |
| |-- 订单4 --------------------->|
| | | |
| |<-- ack 订单1 ----| |
| |<-- ack 订单2 -----------------|
注意:
- 同一时刻订单 1 只会被 A 拿,不会同时给 B
- RabbitMQ 默认是轮询分:A 拿 1、3;B 拿 2、4
- 如果加了
prefetch_count=1(公平分发):A 处理慢的话,B 不会闲着把剩下的全拿走,而是等 A 处理完再拿下一个
2.3 代码
完整代码在 02_workqueue/。
producer.py(前台):
python
import pika, time
credentials = pika.PlainCredentials('admin', '123456')
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='192.168.121.128', port=5672, credentials=credentials)
)
channel = connection.channel()
channel.queue_declare(queue='task_queue', durable=True, exclusive=False, auto_delete=False)
# 模拟派发 5 个任务,每个任务的"耗时"用消息内容表示(秒)
for t in [1, 2, 3, 4, 5]:
channel.basic_publish(
exchange='',
routing_key='task_queue',
body=str(t),
properties=pika.BasicProperties(delivery_mode=2),
)
print(f" [x] 派发任务: {t} 秒")
time.sleep(0.3)
consumer.py (客服,加了 prefetch_count=1 公平分发):
python
import pika, time, sys
credentials = pika.PlainCredentials('admin', '123456')
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='192.168.121.128', port=5672, credentials=credentials)
)
channel = connection.channel()
channel.queue_declare(queue='task_queue', durable=True, exclusive=False, auto_delete=False)
# 关键:公平分发
channel.basic_qos(prefetch_count=1) # 一次只拿 1 个,处理完再拿下一个
def callback(ch, method, properties, body):
duration = int(body.decode())
name = f"[worker-{sys.argv[1]}]" if len(sys.argv) > 1 else "[worker]"
print(f" {name} 收到任务: {duration} 秒")
time.sleep(duration) # 模拟实际处理
print(f" {name} 完成")
ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_consume(queue='task_queue', on_message_callback=callback, auto_ack=False)
channel.start_consuming()
2.4 动手跑
开 3 个 worker:
bash
# 终端 A
.venv/Scripts/python.exe 02_workqueue/consumer.py A
# 终端 B
.venv/Scripts/python.exe 02_workqueue/consumer.py B
# 终端 C
.venv/Scripts/python.exe 02_workqueue/consumer.py C
再开一个生产者:
bash
# 终端 D
.venv/Scripts/python.exe 02_workqueue/producer.py
观察 5 个任务(耗时 1~5 秒)是怎么被 3 个 worker 抢的:
| 行为 | 时间线 |
|---|---|
没有 prefetch_count=1 |
启动时轮询:worker-A 拿 1、4,worker-B 拿 2、5,worker-C 拿 3。A 干 5 秒,B 干 7 秒,C 干 3 秒------慢 worker 拖后腿 |
有 prefetch_count=1 |
A 拿 1(干 1 秒),B 拿 2(干 2 秒),C 拿 3(干 3 秒)。C 干完立刻拿 4,A 干完拿 5。整体完成时间更均衡 |
2.5 小结
| 关键点 | 说明 |
|---|---|
| 多个 consumer 订阅同一个队列 | 这是 WorkQueue 的标志 |
prefetch_count=1 |
公平分发,避免一个 worker 拿光所有任务 |
auto_ack=False + basic_ack |
配合 prefetch 实现"干完才给下一个" |
| 一条消息只被一个 consumer 消费 | 这是和 Pub/Sub 的本质区别 |
3. Pub/Sub:广播通知
3.1 场景
公司群里发了个通知:
- 老板在群里发:"今天下午 3 点开会"
- 所有员工(市场部、研发部、运营部)都收到这条消息
这就是 Pub/Sub 模式:
- 1 个生产者 发到交换机(不是直接发到队列)
- 交换机类型是
fanout(扇出)------ 收到一条就发给所有绑定的队列 - 每个消费者 有自己独立的队列(互不影响)
- 一条消息会被所有消费者都收到
3.2 图解
Producer Exchange (fanout) Queue-A Queue-B Queue-C
(老板) logs | | |
| | | | |
|-- "下午3点开会" ------>| | | |
| |--- 广播 ----------------> | |
| |--- 广播 ---------------------------> |
| |--- 广播 ------------------------------------>|
| | | | |
| | v v v
| | | |
| Consumer-1 Consumer-2 Consumer-3
| (市场部) (研发部) (运营部)
和 WorkQueue 的关键区别:
| 维度 | WorkQueue | Pub/Sub |
|---|---|---|
| 队列数 | 1 个共享队列 | 每个消费者 1 个独立队列 |
| 一条消息被消费 | 1 次(被某个 worker 抢走) | N 次(每个消费者都收一份) |
| 形象比喻 | 多个客服抢同一叠订单 | 老板群里发通知,所有部门都收 |
3.3 代码
完整代码在 03_pubsub/。
producer.py(老板端):
python
import pika
credentials = pika.PlainCredentials('admin', '123456')
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='192.168.121.128', port=5672, credentials=credentials)
)
channel = connection.channel()
# 声明 fanout 交换机
channel.exchange_declare(exchange='logs', exchange_type='fanout', durable=True)
# 发消息到交换机(注意 routing_key='', fanout 模式忽略路由键)
channel.basic_publish(
exchange='logs',
routing_key='',
body='今天下午3点开会',
)
print(" [x] 已广播: 今天下午3点开会")
connection.close()
consumer.py(员工端,每个都自建一个匿名队列):
python
import pika, sys
credentials = pika.PlainCredentials('admin', '123456')
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='192.168.121.128', port=5672, credentials=credentials)
)
channel = connection.channel()
# 同样的交换机
channel.exchange_declare(exchange='logs', exchange_type='fanout', durable=True)
# 关键:声明一个匿名队列
result = channel.queue_declare(queue='', exclusive=True, auto_delete=True)
queue_name = result.method.queue # 拿到 RabbitMQ 自动生成的名字
# 绑定到交换机
channel.queue_bind(exchange='logs', queue=queue_name)
name = f"[{sys.argv[1]}]" if len(sys.argv) > 1 else "[subscriber]"
def callback(ch, method, properties, body):
print(f" {name} 收到广播: {body.decode('utf-8')}")
ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=False)
print(f" {name} 等待广播中...")
channel.start_consuming()
3.4 动手跑
开 2 个员工端:
bash
# 终端 A
.venv/Scripts/python.exe 03_pubsub/consumer.py A
# 终端 B
.venv/Scripts/python.exe 03_pubsub/consumer.py B
再开一个老板端:
bash
# 终端 C
.venv/Scripts/python.exe 03_pubsub/producer.py
A 和 B 同时 打印 今天下午3点开会。
- 神奇之处:A 和 B 是两个独立进程,互不通信,但都收到了同一条消息
- 关键:每个 consumer 启动时都新建了一个匿名队列 (叫
amq.gen-xxx),并 bind 到logs交换机 - 你开 N 个 consumer,就发 N 份出去
3.5 小结
| 关键点 | 说明 |
|---|---|
exchange_type='fanout' |
扇出交换机,消息无脑广播 |
queue='' + exclusive=True |
消费者自建匿名队列,断开即删 |
queue_bind |
把队列"挂"到交换机上,才能收到它的消息 |
| 一条消息被所有消费者收到 | 这是和 WorkQueue 的本质区别 |
| 经典场景 | 系统通知、实时日志、配置变更广播 |
4. 三种模式对比
| 维度 | HelloWorld | WorkQueue | Pub/Sub |
|---|---|---|---|
| 交换机 | 默认 | 默认 | fanout |
| 队列数 | 1 个命名 | 1 个命名 | 每个消费者 1 个匿名 |
| 消费者数 | 1 | 1~N | 1~N |
| 一条消息被消费 | 1 次 | 1 次(被某个 worker 抢走) | N 次(每个消费者都收) |
| 核心机制 | 点对点 | 竞争消费 | 广播 |
| 经典场景 | 通知、命令 | 订单处理、邮件发送 | 系统通知、日志广播 |
记忆口诀:
- HelloWorld :一对一,老师对小明
- WorkQueue :多对一抢,多客服抢订单
- Pub/Sub :一对多广播,老板群里发通知
5. 常见坑
5.1 pika 1.4+ 的 541 错误
pika.exceptions.ConnectionClosedByBroker: (541, 'INTERNAL_ERROR - Feature `transient_nonexcl_queues` is deprecated...)
原因 :pika 1.4 改了 queue_declare 的默认行为,新版 RabbitMQ 不再允许省略三个参数。
解决 :永远显式传 durable / exclusive / auto_delete:
python
channel.queue_declare(queue='xxx', durable=True, exclusive=False, auto_delete=False)
5.2 消息堆积
如果消费者处理太慢,队列里消息会越积越多。两种处理:
- 加更多 consumer(WorkQueue 思路)
- 提高 consumer 处理速度
5.3 消息丢失
要彻底不丢消息,必须三件套:
- 队列
durable=True(队列持久化) - 消息
delivery_mode=2(消息持久化) - consumer
auto_ack=False+ 处理完再basic_ack(消费确认)
少一个都可能在 RabbitMQ 重启时丢消息。
5.4 consumer 启动比 producer 早
没问题 。RabbitMQ 会把消息先存在队列里,consumer 一上线就拿走。这是消息队列相比"直接调用"的核心优势------异步、解耦。
6. 下一步学什么?
学完这 3 种,你可以继续:
- Routing (
direct交换机):按消息的"标签"精确派发,比如"只给运维发系统告警" - Topics (
topic交换机):用通配符订阅,比如"系统.*"、"*.告警" - RPC(基于 RabbitMQ 做远程调用):让消息能"带回结果"
7. 参考
- RabbitMQ 官方教程 - Python
- 本文完整代码:项目地址
如果这篇文章帮到了你,欢迎点赞、收藏、转发。你的支持是我继续写下去的动力 🙌