阶段二的前五篇,分别讲了四种 Exchange、RoutingKey 设计、死信 TTL、队列进阶形态、临时队列。这些是路由设计的「零件」。这一篇把它们组装起来,看在真实业务场景里,路由拓扑该怎么设计。

路由设计是 RabbitMQ 最考验架构能力的部分——不是会不会用某个 Exchange,而是能不能把业务需求翻译成合适的拓扑。同一个「订单事件通知多方」的需求,可以设计得很优雅(一个 topic Exchange + 几个绑定),也可以设计得很啰嗦(一堆 direct Exchange 互相转发)。差别就在于有没有路由设计的全局观。

这一篇用四个典型场景演示设计思路,并总结几个反模式。

场景一:多方订阅同一事件(发布订阅)

需求:订单创建后,积分、通知、风控、统计四个服务都要处理这个事件。

错误设计:建四个 direct Exchange(积分 exchange、通知 exchange……),生产者往四个 exchange 各发一次。问题:生产者要认识所有下游,加一个下游就要改生产者,完全没起到解耦作用。

正确设计:建一个 topic Exchange(order.events),四个服务各建自己的队列绑到它。

生产者 → order.events (topic Exchange)
            ├→ credit.queue    (bind: order.created)   → 积分服务
            ├→ notify.queue    (bind: order.#)          → 通知服务
            ├→ risk.queue      (bind: order.created)    → 风控服务
            └→ stats.queue     (bind: order.#)          → 统计服务

生产者只发一条 order.created 消息到 order.events,Exchange 复制到所有匹配的队列。加新下游 = 加一个队列 + 一个绑定,生产者零改动。这是 Pub/Sub 的标准拓扑。

关键点:用 topic 而非 fanout。虽然现在看起来像广播,但用 topic 保留了将来「选择性订阅」的能力——如果统计服务以后只想收 order.created 不要 order.refund,改绑定就行,不用换 Exchange。

场景二:按优先级处理任务(优先级 + 工作队列)

需求:一个客服工单系统,普通工单和紧急工单都进同一个处理流程,但紧急工单要优先处理;同时要有多个客服 worker 分担。

设计:单个优先级工作队列,多消费者。

生产者 → ticket.exchange (direct)
            └→ ticket.queue (x-max-priority=10)
                  ↑ 多个客服 worker 消费(共享队列,轮询 + prefetch)
  • 队列设 x-max-priority=10
  • 普通工单发 priority=0,紧急工单发 priority=9。
  • 多个 worker 共享队列,prefetch=1,能者多劳。
  • 紧急工单因为 priority 高,会插到普通工单前面被优先消费。

关键点:优先级和工作队列可以共存。多消费者共享优先级队列时,RabbitMQ 按优先级从高到低分发,同优先级内轮询。不要为了「优先级」而拆成两个队列(普通队列 + 紧急队列)——那样还要自己写调度逻辑,反而复杂。

场景三:失败重试与死信归档(可靠性闭环)

需求:处理订单消息可能因为下游短暂故障失败,需要自动重试(指数退避),重试若干次仍失败则进入死信队列人工处理。

设计:业务队列 + 多级延迟重试队列 + 死信归档队列。

业务队列 order.queue
  ↓ 消费失败 nack(requeue=false), x-dead-letter-exchange=retry.exchange
retry.exchange (direct)
  ├→ retry.10s.queue  (TTL=10s,  headers retry=1, DLX=业务exchange) 
  ├→ retry.60s.queue  (TTL=60s,  headers retry=2, DLX=业务exchange)
  └→ retry.300s.queue (TTL=300s, headers retry=3, DLX=死信exchange)
死信归档 dead.queue (消费者:人工/告警)

流程:消费失败 → 死信到 retry.exchange → 进 retry.10s 队列等 10s → TTL 过期死信回业务队列重试 → 再失败进 retry.60s 等更长 → 最多重试 3 次 → 仍失败进 dead.queue 人工处理。

关键点:用 headers 带重试次数,避免无限循环。消费者每次检查消息 headers 里的 retry 次数,决定投到哪一级重试队列,超过上限就不再重试。

这个模式很经典,但也有代价——拓扑复杂,队列多。如果业务对重试要求简单,可以用延迟插件(rabbitmq_delayed_message_exchange)简化:失败的消息带 x-delay 头重投,省去多级 TTL 队列。

场景四:多租户隔离与全局监控(vhost + 路由)

需求:一个 SaaS 平台,每个租户的事件要互相隔离,同时平台侧要有一个全局监控队列收所有租户的异常事件。

设计:vhost 隔离租户 + topic 路由做全局监控。

vhost: tenant-a
  tenant-a.events (topic)
    ├→ tenant-a 业务队列(仅租户 a 可见)
    └→ (可选)绑到监控 exchange

vhost: tenant-b
  tenant-b.events (topic)
    └→ tenant-b 业务队列(仅租户 b 可见)

vhost: platform
  platform.monitor (topic)
    ← Federation 从各租户 vhost 拉取 #.error 事件
    └→ 全局告警队列

租户间用 vhost 做逻辑隔离(权限、命名空间独立),平台监控用 Federation(阶段五详谈)跨 vhost 聚合所有 .error 事件。

关键点:vhost 管隔离,Exchange 管路由,两者各司其职。不要试图用 RoutingKey 做租户隔离(tenant-a.order.created 这种)——RoutingKey 是消息属性,不是权限边界,任何消费者都能订阅。租户隔离必须靠 vhost 的权限机制。

四个场景的路由拓扑对比

路由设计的几个反模式

把这些场景里反复出现的坑总结成反模式:

反模式一:一个 Exchange 走天下。 所有业务共用一个 Exchange,靠 RoutingKey 区分。问题:一个业务改路由规则可能影响其他业务,且 Exchange 成为单点耦合。正确做法:按业务域分 Exchange(order.eventsuser.eventsinventory.events)。

反模式二:Exchange 之间互相转发。 设计成「A exchange 转发给 B exchange,B 再转发给 C」。RabbitMQ 支持这种链式转发,但调试噩梦——消息在哪一层丢了难定位。正确做法:尽量扁平化,一个 Exchange 直接路由到目标队列。

反模式三:用队列名承载业务含义。 把队列命名为 order_vip_user_payment_success,靠队列名表达业务逻辑。问题:队列名一长就难维护,且改业务语义要改队列名(=重建队列)。正确做法:业务语义放 RoutingKey 或消息体,队列名保持简单稳定(payment.queue)。

反模式四:过度使用死信重试链。 重试队列建了五六级,每级不同 TTL。问题:拓扑极难理解和维护。正确做法:重试链最多两三级,更复杂的重试逻辑交给业务层重试框架(如 Spring Retry)。

收束:路由设计是架构能力的体现

路由设计没有标准答案,但有清晰的判断框架:先看分发需求(广播/选择/优先级/重试),再选零件(Exchange 类型 + 队列形态),最后画拓扑。好的拓扑用最少的 Exchange 和队列表达最灵活的路由,且加新需求时改动最小。

阶段二到这里结束。下一阶段会从「功能层」下沉到「实现层」,看 RabbitMQ 的消息到底是怎么存储的——经典队列的内存与磁盘取舍、为什么镜像队列被废弃、Quorum Queue 怎么用 Raft 重做可靠性。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。我相信 AI 是程序员的最佳搭档。想跟完这套「图解 RabbitMQ」,欢迎关注公众号 「十三Tech」

十三Tech公众号二维码