Skip to content

RabbitMQ 核心概念与工作模式 ​

S4-013建议学进阶约 90 分钟JDK 21企业开发进阶 · 消息队列

1. 本节定位 ​

《消息队列基础与选型》讲了"为什么要 MQ";本节讲 RabbitMQ 的灵魂:路由模型:

text
生产者 → Exchange(交换机)→ (Binding + Routing Key) → Queue → 消费者
                ↑
        所有路由逻辑都在这里

★ 把 Exchange 搞懂,RabbitMQ 就懂了一半 ✓✓

2. 学习目标 ​

  • 能说清 Exchange / Binding / Routing Key / Queue 四者的关系 ★★★;
  • 能说出三种 exchange 的路由行为,并知道 * 与 # 的区别 ★★★;
  • 知道「没有队列接收的消息会被丢弃」这个 AMQP 特性 ★★★;
  • 知道工作队列(竞争消费)为什么可能分配不均,以及 prefetch 的作用 ★★★;
  • 会查绑定关系来排障 ★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个真实排障案例 ✗:

text
现象:下单后积分没加、短信也没发,但订单服务日志显示"消息发送成功" ✗
排查:rabbitmqctl list_bindings 一看 ——
      **交换机的绑定关系写错了 routing key**(order.created vs order.create)
根因:RabbitMQ 在「没有队列接收」时**不报错,直接丢弃** ✗✗

★ 本节的实测会复现这个行为 ✓:

text
发送 rk=order.paid       → 投递到 [queue.order.paid, queue.order.paid.urgent]
发送 rk=order.shipped    → 投递到 [queue.order.shipped]
发送 rk=order.cancelled  → **没有队列接收,消息被丢弃** ✗

★ 注意最后一行:生产者的 publish 调用是成功的、没有任何异常 ✗✗

5. 核心概念 ​

5.1 四个核心概念的职责 ★★★ ​

概念职责类比
Exchange ★★★决定"这条消息该去哪些队列"邮局分拣中心 ✓
Binding ★★★把 Exchange 和 Queue 连起来的规则分拣规则表 ✓
Routing Key ★★生产者给消息贴的"地址标签"信封上的地址 ✓
Queue ★★★真正存消息的地方收件人的信箱 ✓

★ 最关键的一步认识 ✓✓:

生产者从不直接把消息发给队列 ✓

它只发给 Exchange,并带一个 routing key ——由 Exchange 按绑定规则决定投递给谁 ✓✓

★ 这与 Kafka 完全不同 ✓:

KafkaRabbitMQ
发送目标直接发到 topic 的某个分区发给 Exchange,由它路由 ✓
消息留存存下来,谁要谁来读(可重放) ✓投递出去就没了(除非声明了持久化队列)✗
没人接收时留在分区里直接丢弃 ✗✗

5.2 direct:精确匹配 ★★ ​

实测 ✓:

text
exchange: order.direct(类型 direct)
绑定:
  order.paid    → queue.order.paid
  order.shipped → queue.order.shipped
  order.paid    → queue.order.paid.urgent      ← 同一个 key 绑了多个队列

发送 rk=order.paid       → [queue.order.paid, queue.order.paid.urgent]  (一份消息,两方消费)
发送 rk=order.shipped    → [queue.order.shipped]
发送 rk=order.cancelled  → **没有队列接收,消息被丢弃** ✗

★ 两个要点 ✓✓:

要点说明
同一个 routing key 可以绑多个队列 ★★一份消息被多方消费(这实现了"发布订阅")✓
匹配不上就丢弃 ★★★不报错、不重试、不落盘 ✗✗

★ 后者是 RabbitMQ 最容易出的事故 ✓ ——生产环境的标准做法:

text
给交换机配 alternate-exchange(备用交换机):
没有队列接收的消息会被转发到备用交换机 → 落到一个"未路由"队列里 → 有人监控 ✓✓

5.3 fanout:广播(忽略 routing key)★★ ​

实测 ✓:

text
exchange: order.fanout(类型 fanout)
绑定:三个队列(其中 app 被绑了两次)

发送 rk=任意key → [queue.notify.sms, queue.notify.email, queue.notify.app, queue.notify.app]

★ 注意两点 ✓:

现象原因
routing key 是"任意key"也照样投递 ✓fanout 完全忽略 routing key ✓
app 队列在列表里出现了两次 ⚠️但实际只收到一条消息 —— binding 是集合语义,重复绑定不产生重复投递 ✓

★ fanout 的典型用途 ✓✓:

一个事件、多个下游各自处理 ✓

如「订单支付成功」要通知:短信、邮件、App 推送、积分、数据仓库 ✓

注意:这与"工作队列"不同 ——fanout 是每个下游都处理一遍 ✓✓

5.4 topic:通配符路由(最灵活)★★★ ​

实测(这张表精确验证了 * 与 # 的语义) ✓✓:

text
绑定:
  *.*.error     → queue.log.all-error        (任意服务、任意模块的 error)
  order.#       → queue.log.order-*          (order 下的所有日志)
  order.*.error → queue.log.order-error      (order 下任意模块的 error)

路由结果:
  order.pay.error          → all-error, order-*, order-error    (三个都命中)
  order.pay.info           → order-*                            (不是 error)
  order.stock.timeout.error → order-*                           ← ★ 关键
  user.login.error         → all-error                          (别的服务)

★★ 第三行是本节最重要的证据 ✓✓:

order.*.error 没有匹配 order.stock.timeout.error ✓

因为 * 恰好匹配一个词 ——而 order.stock.timeout.error 在 order 与 error 之间有两个词 ✓✓

★ 两条规则(面试常考) ✓✓:

通配符含义例子
* ★★★恰好匹配一个词order.*.error 匹配 order.pay.error,不匹配 order.stock.timeout.error ✓
# ★★★匹配零到多个词order.# 连 order 本身都能匹配 ✓

★ 实践建议 ✓:

用一套 routing key 编码业务维度:服务.模块.级别(如 order.pay.error)✓

消费方按需订阅 ——消费方完全不需要知道生产者是谁 ✓✓

代价:topic 路由比 direct 慢(要逐词匹配)—— 但通常不是瓶颈 ⚠️

5.5 工作队列:竞争消费与"不均"问题 ★★★ ​

实测(3 个消费者抢同一个队列的 12 条任务) ✓:

text
  消费者1  收到 10 条:[任务-1, 任务-4, 任务-5, ... 任务-12]
  消费者2  收到  2 条:[任务-2, 任务-3]
  消费者3  收到  0 条:[]
  队列深度:0 | 总交付:12 条(不重复不遗漏)✓

★ 两条结论 ✓✓:

结论说明
不重复不遗漏 ✓竞争消费保证一条消息只被处理一次 ✓
但分配可能极不均匀 ✗✗3 个消费者收到 10 / 2 / 0 ——完全不是平均分配 ✓

★★ 为什么不均 ✓✓:

谁先抢到谁处理 ——消费者 1 恰好跑得快(或调度器一直给它时间片) ✓

于是它一直抢,其他两个饿着 ✓

★★★ 这正是 basic.qos(prefetchCount) 要解决的问题 ✓✓:

java
// ★ 关键配置:一个消费者最多同时持有 1 条未确认的消息
channel.basicQos(1);
配置行为
prefetchCount = 0(默认) ✗不限制 ——快消费者会把消息全抢光 ✗✗
prefetchCount = 1 ★★★处理完一条才给下一条 → 慢消费者也能分到 ✓✓
prefetchCount = N ✓兼顾吞吐与公平(N 通常取 10~100)

★ 但它有个代价 ✓:

prefetch 越小,吞吐越低(消费者经常在等消息)✗

所以是"公平 vs 吞吐"的取舍 ——默认值 0 在"所有消费者性能一致"时反而最优 ✓✓

5.6 五种工作模式的对照 ★★ ​

模式用哪种 exchange一条消息被处理几次
简单队列 ★(默认交换机,直接发队列)1 次
工作队列(竞争消费) ★★(默认交换机)1 次(多个消费者分摊)✓
发布订阅 ★★fanoutN 次(每个队列一次)✓
路由模式 ★★direct按 key 决定
主题模式 ★★★topic按通配符决定 ✓

★ 最容易搞混的是第 2 和第 3 行 ✓✓:

想让"一条消息只被处理一次"→ 工作队列(多消费者同一队列) ✓

想让"一条消息被多方各处理一次"→ fanout(多队列) ✓

判断依据不是"有几个消费者",而是"有几个队列" ✓✓

5.7 排障:先查绑定 ★★★ ​

★ 一条排障经验 ✓✓:

收到消息但没进队列 → 先用 list_bindings 查绑定与 routing key ✓

这是 RabbitMQ 最常见的问题(绑定写错、key 大小写不一致、exchange 类型选错)✓✓

★ 常用运维命令 ✓:

bash
rabbitmqctl list_exchanges name type                     # 看所有交换机
rabbitmqctl list_queues name messages consumers           # ★ 看队列深度与消费者数
rabbitmqctl list_bindings                                # ★ 看绑定关系
rabbitmqctl list_connections name state                  # 看连接
rabbitmq-plugins enable rabbitmq_management              # 开 Web 管理台(15672)

★ 三个必须监控的指标 ✓:

指标含义异常表现
队列深度(messages) ★★★积压量持续上涨 = 消费跟不上了 ✗
消费者数(consumers) ★★有几个在消费掉到 0 = 消费者全挂了 ✗✗
未确认消息数(unacked) ★★投出去但没 ACK 的很多 = 消费者卡住了 ⚠️

6. 递进示例 ​

6.1 最小示例:声明与绑定 ​

java
Channel channel = connection.createChannel();

// 声明交换机与队列(幂等操作,重复声明无害)
channel.exchangeDeclare("order.direct", BuiltinExchangeType.DIRECT, true);
channel.queueDeclare("queue.order.paid", true, false, false, null);

// 绑定:routing key = order.paid 的消息进这个队列
channel.queueBind("queue.order.paid", "order.direct", "order.paid");

// 发送
channel.basicPublish("order.direct", "order.paid", null, body);

6.2 业务示例:一个事件多方消费 ​

java
// 交换机用 fanout,每个下游一个队列
channel.exchangeDeclare("order.paid.fanout", BuiltinExchangeType.FANOUT, true);
for (String consumer : List.of("points", "sms", "email", "warehouse")) {
    channel.queueDeclare("queue." + consumer, true, false, false, null);
    channel.queueBind("queue." + consumer, "order.paid.fanout", "");
}

★ 好处 ✓:加一个下游只加队列与绑定,生产者的代码一行不改 ✓✓(呼应 《SOLID 原则》 的开闭原则)

6.3 反例:用 topic 却把 key 写得很随意 ​

text
✗ key = "orderPaid"(没有分隔),却绑定了 "order.*"
✓ key = "order.paid"(用 . 分隔,便于按维度订阅)✓✓

7. 完整可运行代码与实测 ​

代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/mq/RabbitMqModelLesson.java

运行:

powershell
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.mq.RabbitMqModelLesson

实测输出(关键部分):

text
===== 1. direct exchange =====
  发送 rk=order.paid       → 投递到 [queue.order.paid, queue.order.paid.urgent]
  发送 rk=order.cancelled  → **没有队列接收,消息被丢弃** ✗

===== 3. topic exchange =====
  order.pay.error           → all-error, order-*, order-error
  order.pay.info            → order-*
  order.stock.timeout.error → order-*
  user.login.error          → all-error

===== 4. 工作队列模式 =====
  消费者1  收到 10 条:[任务-1, 任务-4, ...]
  消费者2  收到  2 条:[任务-2, 任务-3]
  消费者3  收到  0 条:[]

★ 三条可落地的结论 ✓:

#结论依据
①匹配不上的消息会被静默丢弃 ——必须配备用交换机或监控order.cancelled 被丢弃 ✗
②* 只匹配一个词order.*.error 没匹配 order.stock.timeout.error ✓
③竞争消费可能极不均匀,要靠 basicQos 纠正10 / 2 / 0 ✗

8. Debug 观察点 ​

  1. 把一个队列的绑定删掉:观察消息静默消失(生产者无感知)✗✗;
  2. 给同一个 routing key 绑两个队列:验证一份消息被两方消费 ✓;
  3. 把 order.*.error 改成 order.#.error:观察 order.stock.timeout.error 也被匹配 ✓✓;
  4. 把 basicQos(1) 去掉:观察快消费者抢光消息 ✗。

9. 常见错误 ​

#错误说明
1以为匹配不上会报错 ✗✗静默丢弃(5.2)✗✗
2把 * 当成"任意多词" ✗✗* 是一个词,# 才是零到多 ✓
3以为"多个消费者同一队列"= 发布订阅 ✗✗那是竞争消费(一条只被处理一次)✓
4忘了配 basicQos ✗快消费者抢光,慢的饿着 ✓
5routing key 不用分隔符 ✗无法按维度订阅 ✓
6不监控队列深度与消费者数 ✗✗积压/消费者全挂都不知道 ✓
7以为 RabbitMQ 能重放消息 ✗✗它是"投递即出队"(Kafka 才能重放)✓

10. 动手实验 ​

目标:用路由表把"一个事件、多方消费"配出来。

步骤:

  1. 跑 RabbitMqModelLesson,确认三种 exchange 的路由结果 ✓;
  2. 为"订单支付成功"设计一套 routing key,覆盖短信/积分/仓配/数据四个下游 ★★;
  3. 配一个 topic 交换机,让"仓配"只订阅"已支付且已发货" ✓;
  4. 验证"没人订阅时消息会被丢弃",并加上备用交换机的兜底设计 ★★★;
  5. 给工作队列加 basicQos(1),观察分配是否变均匀 ✓。

验收方式:能说出"你这个系统里哪些事件适合 fanout、哪些适合 topic"。

扩展任务:设计一个"延迟队列"(RabbitMQ 原生不支持,要想办法):

  • 方案一:TTL + 死信队列(消息设 TTL,过期后进 DLQ 被消费)✓;
  • 方案二:延迟插件 rabbitmq_delayed_message_exchange ✓;
  • 对比两者的优缺点(提示:TTL 方案有个"队头阻塞"陷阱 ——消息按序过期)★★

11. 练习题 ​

  1. 判断:RabbitMQ 中匹配不上任何队列的消息会报错。(对 / 错)
  2. 填空:* 匹配 ______ 个词,# 匹配 ______ 个词。
  3. 简答:工作队列模式与发布订阅模式的区别是什么?
  4. (场景判断)
text
routing key = order.stock.timeout.error
绑定之一:order.*.error

会匹配吗?为什么? 5. (设计)给"订单状态变更"设计路由方案:短信服务只关心已支付,仓配只关心已发货,数据仓库关心全部。

参考答案(要点)
  1. 错。会被静默丢弃,生产者毫无感知 ✗✗ ——所以要配备用交换机或监控 ✓★
  2. 恰好一个;零到多个 ✓
  3. 工作队列 = 多个消费者抢同一个队列 → 一条消息只被处理一次 ✓; 发布订阅 = 一条消息进多个队列 → 每个队列各处理一次 ✓★
  4. 不匹配 ✗ ——因为 * 只匹配一个词,而中间有两个词(stock、timeout) ✓ —— 要用 order.#.error 或 order.stock.*.error ✓★
  5. 要点:topic 交换机,key 形如 order.paid / order.shipped ✓; 短信绑 order.paid、仓配绑 order.shipped、数仓绑 order.# ✓✓

12. 掌握标准 ​

  • [ ] 能说清 Exchange / Binding / Routing Key / Queue 的关系 ★★★;
  • [ ] 能说出三种 exchange 的行为差异 ★★★;
  • [ ] 精确掌握 *(一个词)与 #(零到多词) ★★★;
  • [ ] 知道"匹配不上会静默丢弃",并知道怎么兜底 ★★★;
  • [ ] 知道竞争消费的不均问题与 basicQos 的作用 ★★★;
  • [ ] 知道 RabbitMQ 不能重放消息(与 Kafka 的本质区别) ★★;
  • [ ] 会查绑定关系来排障 ★★。

13. 扩展资源 ​

14. 下一步学习 ​

《消息可靠性、幂等与顺序》:丢不丢、重不重、乱不乱。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引