RabbitMQ 路由设计:Exchange 与 Queue 怎么选(三)
系列导航:
- 上一篇:RabbitMQ 核心模型:一条消息如何完成路由与消费(二)
- 当前篇:RabbitMQ 路由设计:Exchange 与 Queue 怎么选(三)(本文)
- 下一篇:RabbitMQ 消费控制:ack、nack、prefetch 与消息确认(四)
到了第三篇 overview,问题已经从“为什么需要消息队列”与“消息如何流转”进入到真正影响业务建模的部分:拓扑怎么设计。也就是,消息先发到哪种 Exchange、应该建几个 Queue、多个消费者到底是在竞争消费还是在做发布订阅。
本文只回答这些业务拓扑问题,不展开消费确认、prefetch、重试、顺序和生产可靠性。你可以把它理解成 RabbitMQ 的“结构设计篇”。
继续使用 order.created 这个共享示例
三篇 overview 文章都使用同一个事件:
{
"eventId": "01J...",
"eventType": "order.created",
"occurredAt": "2026-07-15T10:00:00Z",
"orderId": "ORD-20260715-001",
"customerId": "C10086"
}
假设订单服务发布这个事件后,有三个下游业务:
- 库存系统要预留库存
- 通知系统要发送消息
- 审计系统要记录事件
真正要设计的不是“消息有没有发出去”,而是下面三个问题:
- 这三个业务应该共用一个 Queue,还是各自独立?
- 订单事件应该发到哪种 Exchange?
- 某个业务如果有多个实例,它们之间算一份订阅还是多份订阅?
第一原则:按业务能力拆独立 Queue
只要两个处理链路的职责不同,就应该优先考虑给它们独立 Queue。
以 order.created 为例,更合理的拓扑通常是:
Exchange: orders.events
-> Queue: inventory.order-events -> 库存服务实例们
-> Queue: notification.order-events -> 通知服务实例们
-> Queue: audit.order-events -> 审计服务实例们
这样设计有四个直接好处:
- 库存积压不会堵住通知
- 审计暂时下线不会影响库存
- 每个业务都能按自己的节奏扩缩容
- 后续新增订阅方时,不需要回头重构已有消费者
一个常见误区是让多个业务直接共用同一个 Queue。这样做虽然省了声明拓扑的步骤,但会把原本应该独立演进的业务硬绑在一起,导致消息分流、回放、扩容、排障都变难。
所以,Queue 首先应该表达业务处理边界,而不是节省几个配置项。
竞争消费和发布订阅不是一回事
理解 Queue 设计时,最容易混淆的就是这两个概念。
竞争消费:多个实例共同处理同一份工作
如果库存服务部署了 3 个实例,并且它们都消费 inventory.order-events,那么它们是在竞争同一个 Queue 里的消息。
这意味着:
- 这 3 个实例属于同一个业务订阅
- 每条消息通常只会被其中一个实例处理
- 目的是提升吞吐和可用性,而不是复制消息
发布订阅:多个业务都要收到一份消息
如果库存、通知、审计都要处理 order.created,那就不是“让三个服务一起抢一个 Queue”,而是应该让 Exchange 把消息分别路由到三个独立 Queue。
这意味着:
- 三个业务是三份独立订阅
- 每个业务都应拥有自己的一份消息副本
- 每个业务内部是否再做多实例竞争消费,是各自的实现问题
一句话区分:
- 同一个业务的多个实例,共享一个 Queue,叫竞争消费
- 不同业务各自拥有独立 Queue,叫发布订阅
Direct、Topic、Fanout、Headers 到底怎么选
Exchange 类型不是“高级版和低级版”的关系,而是不同的路由语义。判断标准不是性能想象,而是业务拓扑到底想表达什么。
Direct|精确匹配一个明确的路由键
当事件名或命令名已经稳定,而且订阅规则只需要判断“是否完全相等”时,Direct 通常最直接。
- 判断信号
- routing key 必须与 binding key 精确相等,不需要通配或多条件组合。
- 典型表达
order.created- 适合场景
- 事件名固定明确的业务事件、任务或命令投递。
- 使用边界
- 事件层级持续增加时,精确绑定会越来越零散,需要维护更多规则。
Topic|按层级模式订阅一类事件
当事件名天然具有业务域和动作层级时,Topic 可以用通配规则表达一类订阅,而不必逐条声明精确绑定。
- 判断信号
- 订阅方想匹配某个业务域、某一层事件,或跨层级的一组事件。
- 典型表达
order.*- 适合场景
- 订单、支付等业务事件按层级命名,并需要按类别订阅。
- 使用边界
- 如果事件命名缺少稳定约定,通配模式会迅速失去可读性和准确性。
Fanout|不做筛选,广播给所有绑定队列
当所有绑定方都应该收到同一条消息时,Fanout 直接复制到每个绑定 Queue,不要求发布方设计路由键。
- 判断信号
- 所有订阅方都需要收到消息,不存在“谁该收到、谁不该收到”的筛选问题。
- 典型表达
routing key 会被忽略- 适合场景
- 配置刷新、缓存失效和系统范围内的统一广播。
- 使用边界
- 一旦不同订阅方需要不同筛选规则,就应该改用 Direct 或 Topic。
Headers|按多个消息头条件组合匹配
当路由依赖地区、租户、优先级或来源系统等多个属性,而且单个路由键难以自然表达时,再考虑 Headers。
- 判断信号
- 匹配条件来自多个消息头,需要按全部满足或任一满足组合判断。
- 典型表达
x-match: all / any- 适合场景
- 多维属性确实无法清晰编码进事件名或 routing key 的路由。
- 使用边界
- 规则更难观察和解释,常规业务事件不应为了灵活性优先采用。
默认 Exchange 的边界:能直达 Queue,不适合做业务拓扑
默认 Exchange 很适合最简单的点对点场景:你已经知道目标 Queue 名称,并且就是想把消息送过去。
例如:
publish(exchange = "", routingKey = "inventory.order-events")
这类方式的边界也很明显:
- 发布方必须知道具体 Queue 名称
- 新增一个下游时,发布方往往也要跟着改
- 不适合表达“一条事件由多个业务分别订阅”的拓扑
所以,默认 Exchange 适合简单投递,不适合承担可演进的业务路由设计。只要你的需求已经进入“多个业务订阅同一个事实”这个层级,就应该更认真地声明显式 Exchange 和独立 Queue。
一个简单够用的选型顺序
如果你不想一上来就陷入术语细节,可以按下面顺序判断:
- 订阅边界
先判断需要几份业务订阅
先区分“同一业务的多个实例”和“多个不同业务”。前者共享一份订阅,后者需要各自收到消息副本。
- Queue 拆分
不同业务拆成独立 Queue
库存、通知和审计分别拥有 Queue;每个业务内部再按吞吐需要部署多个竞争消费者。
- 路由语义
再按订阅语义选择 Exchange
精确值、层级模式、全部广播和多维属性,分别对应
Direct / Topic / Fanout / Headers。 - 最小特例
最后判断是否需要默认 Exchange
只有发布方明确知道唯一目标 Queue,而且不需要演进成多订阅拓扑时,才把默认 Exchange 作为简单直达方案。
这个顺序的重点不是“选出最强的 Exchange”,而是先把业务边界想清楚:谁是一份独立订阅,谁只是同一份订阅下的多个实例。
一个常见的拓扑判断模板
对 order.created 这样的业务事件,很多团队最终会收敛到类似下面的思路:
order.created 的推荐落点
订单服务只发布已经发生的业务事实,Exchange 负责复制,三个业务 Queue 分别承接自己的处理进度。
orders.events- 库存订阅
inventory.order-events - 通知订阅
notification.order-events - 审计订阅
audit.order-events