RabbitMQ 核心原理:一条消息如何完成路由与消费
订单创建后,订单服务往往还要扣减库存、发送通知。如果直接同步调用库存服务和通知服务,调用方会同时承担三个问题:它必须知道下游接口,业务耦合随流程增长;峰值流量会立刻压到每个下游,缺少缓冲;任一服务超时或故障,都可能沿调用链向上游传播。
RabbitMQ 可以把“订单已经创建”变成异步消息,让生产者和消费者在时间与部署上解耦,并通过 Queue 缓冲短时流量。但消息队列不是免费的可靠性开关:系统还要面对重复消息、处理延迟、消息积压,以及 Broker 部署、监控和故障处理等额外运维成本。只有这些代价小于同步链路带来的问题时,引入 RabbitMQ 才有价值。
这个系列分三步建立完整认知:本篇只讲 RabbitMQ 的核心模型;下一篇用 .NET 代码落实发布与消费;第三篇再讨论生产环境中的端到端可靠性。
系列导航:
- RabbitMQ 核心原理:一条消息如何完成路由与消费(本文)
- RabbitMQ + .NET 10 实战:可靠地发布与消费消息
- RabbitMQ 生产实践:可靠性、幂等、重试与高可用
📦 从一个订单事件开始
三篇文章都使用同一个“订单已创建”事件:
{
"eventId": "01J...",
"eventType": "order.created",
"occurredAt": "2026-07-15T10:00:00Z",
"orderId": "ORD-20260715-001",
"customerId": "C10086"
}
订单服务只陈述已经发生的事实,不直接决定谁来处理。库存服务可以据此预留库存,通知服务也可以据此发送消息;两者各自演进,不需要把彼此写进订单服务的同步调用链。
这条消息的拓扑如下:
订单服务 Producer
│ order.created
▼
Topic Exchange: orders.events
│
├─ order.* ──> Queue: inventory.order-events ──> 库存 Consumer
└─ order.# ──> Queue: notification.order-events -> 通知 Consumer
沿路径逐项看,每个节点只有一种主要职责:
- Producer(生产者) 是订单服务中的发布方。它创建消息,并用
order.created作为 Routing Key 发布。 - Connection(连接) 是客户端到 RabbitMQ Broker 的长生命周期 TCP 连接。建立连接需要网络握手和认证,不应为每条消息重复创建。
- Channel(通道) 是复用在 Connection 上的轻量协议通道。发布、声明拓扑和消费等 AMQP 0-9-1 操作都发生在 Channel 中,应用可以避免为每类操作各建一条 TCP 连接。
- Exchange(交换机)
orders.events接收发布,并根据自身类型、Routing Key 与已有 Binding 决定消息应该路由到哪些 Queue。Exchange 的职责是路由,不是保存消息。 - Binding(绑定) 是 Exchange 与目标 Queue 之间的路由规则。这里两条规则分别使用 Binding Key
order.*和order.#。 - Routing Key(路由键) 是 Producer 发布时附带的路由信息。Topic Exchange 会拿
order.created与每条 Binding Key 做模式匹配。 - Queue(队列) 保存已经路由成功、等待投递或重新投递的消息。库存和通知使用两个独立 Queue,因此能各自收到并保留一份消息。
- Consumer(消费者) 从指定 Queue 接收消息并执行业务。消费成功与否最终通过 ack 或 nack 等确认动作反馈给 Broker。
可以把这条路径压缩成一句话:Producer 通过 Connection 上的 Channel 向 Exchange 发布消息,Exchange 按 Binding 和 Routing Key 把消息路由到 Queue,Consumer 再从 Queue 接收并确认消息。
🧭 Exchange、Binding 与 Routing Key
Exchange 的类型决定“如何解释路由规则”。常用的 Direct、Topic 和 Fanout 不是性能等级,而是三种不同的匹配语义。
| Exchange | 匹配方式 | 本文示例 | 典型用途 |
|---|---|---|---|
| Direct | 精确匹配 | order.created | 明确命令或事件 |
| Topic | 单词模式 | order.*、order.# | 事件分类订阅 |
| Fanout | 广播 | 忽略 Routing Key | 缓存失效、广播通知 |
Direct:值完全相等才匹配
Direct Exchange 要求 Binding Key 与消息的 Routing Key 精确匹配。Queue 绑定为 order.created 时,只会匹配同样使用 order.created 的消息,不会匹配 order.cancelled。
它适合路由目标明确的命令或事件。多个 Queue 也可以使用相同 Binding Key 绑定到同一个 Direct Exchange,此时每个匹配 Queue 都会得到一份消息;“精确匹配”并不等于“只能路由到一个 Queue”。
Topic:按点分隔的单词模式匹配
Topic Exchange 把 Routing Key 看成由 . 分隔的若干单词,并使用 Binding Key 表达模式:
*恰好匹配一个单词,所以order.*能匹配order.created,但不能匹配order.eu.created。#匹配零个或多个单词,所以order.#既能匹配order,也能匹配order.created和order.eu.created。
这正是示例选用 Topic Exchange 的原因:库存服务可以只订阅两段式订单事件,通知服务则订阅 order 层级下的所有事件。随着事件类型增加,Producer 不必知道有哪些消费者。
Fanout:不看 Routing Key,向所有绑定目标广播
Fanout Exchange 完全忽略 Routing Key,把每条消息路由到所有绑定 Queue。若库存、通知和审计都必须收到缓存失效事件,就应该为三个订阅方分别建立 Queue,再把它们绑定到同一个 Fanout Exchange。
Headers Exchange 则不依赖 Routing Key,而是根据消息头及绑定参数进行匹配。它适合确实需要多维头部条件的场景;本文不展开其配置和代码,避免把简单的事件分类设计复杂化。
默认 Exchange 为什么看起来像“直接发 Queue”
AMQP 0-9-1 中,Producer 通常把消息发布到 Exchange。RabbitMQ 还预声明了一个特殊的 Direct Exchange,称为 默认 Exchange(Default Exchange)。客户端发布时用空字符串 "" 表示它。
每当声明一个 Queue,RabbitMQ 都会自动以该 Queue 的名称作为 Routing Key,把它隐式绑定到默认 Exchange。因此,向 "" 发布并把 inventory.order-events 作为 Routing Key,消息就能被路由到同名 Queue。底层仍然经过 Exchange,只是绑定由 RabbitMQ 自动创建。
默认 Exchange 很适合“Hello World”式点对点示例,却不能由此得出“RabbitMQ 总是直接把消息发到 Queue”的结论。业务需要可演进的路由规则时,应显式声明合适类型的 Exchange 和 Binding。
🗃️ Queue 保存什么,又不负责什么
Queue 保存待消费消息,是生产速率和消费速率之间的缓冲区。声明 Queue 时常见的三个属性决定它的生命周期:
durable:Broker 重启后恢复 Queue。它首先描述 Queue 元数据的生存能力,不代表其中每条消息都已经安全持久化;消息能否在故障后恢复还取决于消息持久性和 Broker 的可靠性机制。exclusive:Queue 只能由声明它的 Connection 使用,并在该 Connection 关闭时删除。它通常用于客户端实例或会话专属的临时状态。auto-delete:Queue 至少有过一个 Consumer 后,当最后一个 Consumer 取消订阅时自动删除。它与 exclusive 不是同一个概念。
这些属性没有一套适用于所有场景的固定组合。业务 Queue 通常需要稳定名称和跨重启存在;临时订阅则可能使用由 Broker 生成名称的 exclusive、auto-delete Queue。选型应从数据生命周期出发,而不是照抄参数。
Queue 也不负责通用路由。它接收 Exchange 已经匹配并投递给它的消息,之后管理存储、投递和确认状态;事件应该去哪个 Queue,仍由 Exchange 与 Binding 决定。
竞争消费不等于发布订阅
同一个 Queue 可以注册多个 Consumer。RabbitMQ 通常把 Queue 中的每条消息分配给其中一个 Consumer,从而让多个实例分摊工作:
竞争消费:一个 Queue -> Consumer A / Consumer B
如果 Consumer A 和 Consumer B 是库存服务的两个实例,这种竞争消费可以提高库存事件的处理能力,但一条消息通常只由其中一个实例处理。
如果库存业务和通知业务都必须收到订单事件,就不能让它们竞争同一个 Queue。应为每个业务创建独立 Queue,并通过 Exchange 分别路由:
发布订阅:一个 Exchange -> Queue A -> Consumer A
-> Queue B -> Consumer B
Exchange 把消息复制到两个匹配 Queue 后,它们有独立的积压、消费速率和确认状态。一个业务暂停不会阻止另一个业务继续消费。
✅ 消息什么时候才可以删除
消息被投递到 Consumer,不等于业务已经处理成功。RabbitMQ 用消费者确认(Consumer Acknowledgement)区分“已经发送”和“可以删除”。手动确认模式下,一条消息经历以下生命周期:
- Broker 把消息从 Queue 投递给 Consumer。
- 消息进入 unacknowledged 状态:它暂时不再作为 ready 消息投递,但 Broker 仍然保留对它的责任。
- 业务成功后,Consumer 发送
ack,Broker 才可以删除这条消息。 - 业务失败时,Consumer 可以发送
nack或reject。当requeue: true时,消息可以重新入队,等待再次投递。 - 如果 Connection 中断、Channel 关闭或 Consumer 进程退出,尚未确认的消息会自动重新入队并再次投递。
自动确认模式则在消息发送出去后就把它视为成功,准确地说,是写入客户端 TCP Socket 后即可确认。它不是“Consumer 的业务处理成功”。如果进程在收到消息后、完成数据库更新前退出,Broker 已经没有可用于重投递的未确认消息。
手动确认能把删除时点推迟到业务成功之后,但仍然不能消除重复:例如业务操作已成功,而 ack 尚未到达 Broker 时连接中断,消息就会再次投递。Consumer 必须把 redelivery 当作正常情况,为重复消息做好准备;如何实现业务幂等留到系列第三篇。
这里的 ack 只确认 Broker 到 Consumer 的一次投递。它不等于 Producer 用来确认 Broker 是否接管发布的发布者确认,两者方向、时机和解决的问题都不同。
prefetch:给未确认消息设置上限
如果 Broker 不受限制地把消息推给一个处理较慢的 Consumer,大量消息会堆在客户端内存中并处于 unacknowledged 状态,其他 Consumer 反而可能空闲。在本文使用的 RabbitMQ 4.3 语义下,basic.qos 设置 global: false 时,prefetch 按 Consumer 限制允许同时未确认的消息数量。
prefetch 会同时影响三个方面:
- 值较大时,Consumer 更容易持续有活可做,吞吐可能提高,但未确认消息和客户端内存占用也会上升。
- 值较小时,消息更容易在多个 Consumer 间公平分配,但网络往返或处理空档可能限制吞吐。
- 下游数据库或第三方接口容量有限时,prefetch 也间接限制单个 Consumer 同时压向下游的工作量。
不存在通用的最佳数值。更稳妥的原则是从较小值开始,再根据单条消息处理耗时、Consumer 数量、期望并发和下游容量逐步调整。下一篇会把 prefetch 落到 .NET 客户端配置中。
顺序只在特定边界内成立
RabbitMQ Queue 具有 FIFO 特征,并会尽力保持入队和投递顺序,但不能笼统地描述为“全局严格有序”。
多个 Connection 或 Channel 并发发布时,消息可能交错入队;同一 Queue 有多个 Consumer 时,虽然 Broker 依次分发,业务完成时间仍可能不同;消息重投递可能改变 Consumer 观察到的顺序;优先级 Queue 会优先投递高优先级消息;Consumer 自身并发处理也会让完成顺序不同于接收顺序。
因此,需要顺序约束时,必须先说明是同一发布 Channel 的入队顺序、单个 Queue 的投递顺序,还是业务处理完成顺序,再控制发布并发、Consumer 数量、重投递和处理模型。不能用一句“RabbitMQ 保证顺序”代替这些前提。
🎯 最小选型流程
面对一个新场景,可以先用四个问题做最小决策:
- 只需要精确路由:优先选择 Direct Exchange。
- 需要按事件层级订阅:优先选择 Topic Exchange。
- 所有订阅方都应收到:选择 Fanout Exchange,并为每个订阅方创建独立 Queue。
- 需要多个实例分摊同一工作:让这些实例消费同一个 Queue。
最后再检查三个容易混淆的能力边界:Exchange 负责路由,不存储消息;Queue 保存待消费消息,不负责通用路由;ack 确认消费投递,不等于发布者确认。
至此,一条 order.created 的路径就清晰了:订单服务通过长连接上的 Channel 发布到 orders.events,Topic Exchange 依据两个 Binding 把消息分别路由到库存和通知 Queue,各自的 Consumer 在业务成功后确认消息。后续两篇会分别回答“如何用 .NET 正确实现”以及“出现重复、积压和故障时如何建立生产保证”。
下一篇:RabbitMQ + .NET 10 实战:可靠地发布与消费消息