上一篇介绍了四种 Exchange,提到 topic 是用得最多、也最有威力的。这一篇单独把 topic 拆开,讲清楚它的核心——RoutingKey 和 BindingKey 怎么设计

很多人用 topic 用得不好,不是因为不懂通配符 *#,而是没搞清一个根本问题:RoutingKey 不是随便起的名字,它是一套命名规范。设计得好,路由规则简洁、可演化;设计得差,要么通配符匹配不到想要的范围,要么改一个字段就牵动一堆绑定。这一篇把 RoutingKey 的设计原则讲透。

先分清 RoutingKey 和 BindingKey

这两个词经常被混用,但它们是两个不同的东西:

  • RoutingKey生产者发消息时带的。每条消息发出时附带一个 RoutingKey,表示「我是谁」。比如 order.payment.success
  • BindingKey绑定时声明的。把队列绑到 Exchange 时指定一个 BindingKey(带通配符),表示「我要谁」。比如 order.payment.#

Exchange 拿消息的 RoutingKey,去和所有 Binding 的 BindingKey 做匹配,匹配上的队列就收到消息。两者都是点号分隔的字符串,但角色不同:RoutingKey 是消息的标签,BindingKey 是订阅的规则。

理解这个区分很重要,因为后续讲路由设计时,RoutingKey 的设计和 BindingKey 的设计是两件事——RoutingKey 要稳定、可组合(因为它由生产者写死在代码里),BindingKey 要灵活、可演化(因为它由运维通过配置调整)。

点号层级:RoutingKey 的命名规范

topic 的 RoutingKey 必须用点号 . 分隔成多个段。这个「点号层级」不是随意的,它应该反映业务的结构,就像 URL 的路径层级一样。

一个好的 RoutingKey 规范,通常遵循「从粗到细」的层级:

<业务域>.<实体>.<动作>.<结果>

比如:

  • order.payment.success:订单域,支付实体,成功。
  • order.refund.failed:订单域,退款实体,失败。
  • user.register.completed:用户域,注册实体,完成。
  • inventory.stock.low:库存域,库存实体,低位告警。

这种层级的好处是每一层都可以独立做通配

  • order.#:所有订单域事件(一个监控队列全收)。
  • order.payment.#:所有支付事件(支付服务全收)。
  • order.*.success:所有订单成功事件(对账服务收)。
  • #.failed:所有失败事件(告警服务全局监控)。

如果 RoutingKey 设计得好,下游想要什么样的子集,都能用一句 BindingKey 表达。这就是「路由键是命名规范」的实质——好的规范让任何订阅需求都能用通配符自然表达

反过来,如果 RoutingKey 设计得差(比如 order_event_payment_ok_2026 这种扁平字符串),通配符就废了,所有订阅都得精确匹配,失去了 topic 的意义。

通配符 *# 的精确边界

两个通配符的语义要精确记住:

  • *匹配恰好一段。注意是「恰好一段」,不是「一段或多段」。order.*.success 匹配 order.payment.success,但不匹配 order.success(少了一段),也不匹配 order.payment.vip.success(多了一段)。
  • #匹配零段或多段order.# 匹配 order(零段)、order.payment(一段)、order.payment.success.vip(多段)。

最容易踩的坑是把 * 当成「任意」用。比如想「匹配 order 下所有事件」,写成 order.*,结果发现 order.payment.success 匹配不上(因为 * 只匹配一段,而 payment.success 是两段)。正确写法是 order.#

另一个细节:# 可以匹配零段,所以 order.# 能匹配 order 本身(没有任何子段)。这个特性偶尔有用(用 order.# 同时收 orderorder.xxx),但要留意它「什么都收」的副作用。

RoutingKey 设计的几条原则

落到设计,有几条可复用的原则:

原则一:层级稳定,从粗到细。 一旦定下 <业务域>.<实体>.<动作>.<结果> 的层级,就不要轻易改层级数。因为 RoutingKey 写在生产者代码里,改层级意味着改代码、改所有下游的 Binding。可以增加新值(比如加一个新的实体类型),但不要改结构。

原则二:用小写、用名词。 RoutingKey 是机器匹配的,不是给人读的句子。order.payment.successOrderPaymentWasSuccessful 更规范——全小写、点号分隔、名词短语。大小写敏感(Orderorder 是不同的)。

原则三:避免把易变信息塞进 RoutingKey。 比如把 orderId、时间戳塞进 RoutingKey 是反模式——这些是数据,不是分类。RoutingKey 应该是「类别」,便于通配;数据应该放消息体或 headers。

原则四:预留扩展位。 如果业务可能演进,在层级里预留位。比如现在只有 order.payment.success,但未来可能有 VIP 通道,可以一开始就规划 order.payment.<channel>.success 的结构,或者用 order.payment.success.<channel> 在末尾扩展(末尾扩展更安全,因为 order.payment.# 这种绑定不受影响)。

一个完整的路由设计示例

把上面的原则用一个例子串起来。假设一个电商系统,有订单、支付、库存、物流四个域,各域有不同的下游消费:

业务域规划:
  order.payment.<result>          订单支付结果
  order.refund.<result>           订单退款结果
  inventory.stock.<event>         库存事件
  logistics.shipment.<event>      物流事件

下游订阅设计:
  积分服务      ← order.payment.success          (消费成功,加积分)
  通知服务      ← order.#                         (所有订单事件发通知)
  风控服务      ← #.failed                         (所有失败事件做风控)
  库存服务      ← order.payment.success, order.refund.success (扣减/回滚库存)
  数据服务      ← order.#, inventory.#, logistics.# (全量数据同步)

这个设计里,加一个新下游(比如对账服务想收所有 success 事件)只需加一个绑定 *.payment.successorder.*.success,生产者和现有下游都不用改。这就是 RoutingKey 设计得好带来的演化能力。

RoutingKey 层级与通配符匹配示例

收束:RoutingKey 是契约

RoutingKey 表面是字符串,实质是生产者和消费者之间的契约。生产者承诺「我会按这个规范发」,消费者依赖「我能按这个规范订阅」。改 RoutingKey 的结构就是改契约,要协调上下游一起改。

下一篇往另一个方向走,讲消息的「第二次生命」——死信和 TTL,看消息在被消费之前还能经历什么。


关于十三Tech

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

十三Tech公众号二维码