RabbitMQ 三种模式入门:HelloWorld、WorkQueue、PubSub

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 消息丢失

要彻底不丢消息,必须三件套:

  1. 队列 durable=True(队列持久化)
  2. 消息 delivery_mode=2(消息持久化)
  3. consumer auto_ack=False + 处理完再 basic_ack(消费确认)

少一个都可能在 RabbitMQ 重启时丢消息。

5.4 consumer 启动比 producer 早

没问题 。RabbitMQ 会把消息先存在队列里,consumer 一上线就拿走。这是消息队列相比"直接调用"的核心优势------异步、解耦


6. 下一步学什么?

学完这 3 种,你可以继续:

  • Routingdirect 交换机):按消息的"标签"精确派发,比如"只给运维发系统告警"
  • Topicstopic 交换机):用通配符订阅,比如"系统.*"、"*.告警"
  • RPC(基于 RabbitMQ 做远程调用):让消息能"带回结果"

7. 参考


如果这篇文章帮到了你,欢迎点赞、收藏、转发。你的支持是我继续写下去的动力 🙌

相关推荐
霸道流氓气质2 小时前
分布式追踪与 RequestId 传播完全指南
分布式
cheems95272 小时前
[RabbitMQ高级特性] 消息确认机制:从 Ready / Unacked 到 basicAck、basicReject、basicNack 的底层拆解
分布式·rabbitmq·ruby
枫华落尽3 小时前
【Hadoop01-完全分布式运行模式】
分布式
隔壁阿布都3 小时前
ShedLock 分布式定时任务锁框架介绍
spring boot·分布式
文艺倾年3 小时前
【强化学习】数学推导专题,20W字总结(十五)
人工智能·分布式·大模型·强化学习·vibecoding
guslegend3 小时前
第1章:初始Kafka
分布式·kafka
ACP广源盛1392462567320 小时前
GSV5600@ACP#多接口协议转换芯片,物理 AI 便携终端的互联核心
大数据·人工智能·分布式·嵌入式硬件·spark
极客先躯1 天前
高级java每日一道面试题-2026年02月12日-实战篇[Docker]-什么是容器的 Seccomp 配置?如何自定义?
java·运维·分布式·docker·容器·自动化·文件
Francek Chen1 天前
【大数据处理与分析】MapReduce:06 MapReduce编程实践
大数据·hadoop·分布式·mapreduce