2.3 绑定与路由键:路由契约的写法 本节摘要:绑定(Binding)是交换机与队列之间的连线,连线上写的匹配条件就是契约;路由键(Routing Key)是发布方写在消息上的门牌号。两者相遇才完成一次路由判决。本节讲清契约的三种写法、绑定叠加与多对多的拓扑语义,并给出按业务语义设计路由键命名规范的实操方法。 2.2 节的判决实验里,我们其实一直在借另一个主角的力:绑定。交换机类型只决定"怎么比对",可"跟谁比对、比对条件是什么"全由绑定决定。这一节把合约本身摊开来看。 契约的三种写法 第一种,等值契约:direct 交换机下,绑定键与消息路由键全等才生效,比如绑定键 只接待一字不差的门牌号。第二种,模式契约:topic 交换机下,绑定键允许星号与井号通配,等值升级为模式匹配。
本节摘要:绑定(Binding)是交换机与队列之间的连线,连线上写的匹配条件就是契约;路由键(Routing Key)是发布方写在消息上的门牌号。两者相遇才完成一次路由判决。本节讲清契约的三种写法、绑定叠加与多对多的拓扑语义,并给出按业务语义设计路由键命名规范的实操方法。
2.2 节的判决实验里,我们其实一直在借另一个主角的力:绑定。交换机类型只决定"怎么比对",可"跟谁比对、比对条件是什么"全由绑定决定。这一节把合约本身摊开来看。
第一种,等值契约:direct 交换机下,绑定键与消息路由键全等才生效,比如绑定键 order.created 只接待一字不差的门牌号。第二种,模式契约:topic 交换机下,绑定键允许星号与井号通配,等值升级为模式匹配。第三种,空白契约:fanout 交换机下绑定键形同虚设,连线本身就是全部语义——"绑了就投"。
一个容易被误解的事实:绑定的语义完全取决于交换机类型。同一个绑定键 order.*,在 direct 交换机下是一个普通的字面量字符串(要求路由键恰好是这两个字符加一个星号),在 topic 交换机下才是通配模式。绑定从不解释自己,解释权在交换机。
另一个关键性质是叠加:同一队列可以绑多条,同一交换机可以接多队列,多对多自由组合。多条绑定命中同一条消息时,队列只收一份副本;一条消息命中同一交换机下的多个队列,各队列各收一份。这些语义叠加起来,就是构建扇出、过滤、分流三类拓扑的全部积木。
背景:风控组提了个需求:他们只想看"支付失败"事件,不想被全量支付事件打扰。原拓扑是 risk.q 以绑定键 payment.# 挂在 topic 交换机 biz.events 上,全量支付消息涌进风控队列,把真正要紧的失败事件淹没了。
操作:三步改契约。先看现状,用管理命令导出绑定关系:
rabbitmqctl list_bindings source_name kind destination_name routing_key # 预期输出(节选): # biz.events topic risk.q payment.# # biz.events topic points.q #.error
再用代码收紧契约:把 risk.q 的绑定从"支付全域"缩到"失败事件":
import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost")) channel = connection.channel() # 第一步:解绑旧契约(参数必须与当初绑定时完全一致) channel.queue_unbind( exchange="biz.events", queue="risk.q", routing_key="payment.#") # 第二步:绑定新契约,只收失败事件 channel.queue_bind( exchange="biz.events", queue="risk.q", routing_key="payment.*.failed") print("风控契约已切换")
第三步,验证。发布三条消息:payment.created、payment.timeout.failed、payment.refund.failed,随后清点:
rabbitmqctl list_queues name messages # 预期输出: # risk.q 2 # points.q 0
结果:risk.q 收到两条,正是两条以 failed 结尾的支付事件;payment.created 被新契约挡在门外。解读:契约收紧的意义不在省流量,而在信噪比——告警队列里混进一千条正常事件,等于没有告警。另外注意 queue_unbind 的一个细节:它按"交换机、队列、绑定键"三元组精确解绑,如果当初绑定键写错了,解绑语句会静默无效果,排查时要先用上面的 list_bindings 对齐事实,再动手改。
变式一:多契约并存。给 risk.q 再绑一条 payment.*.rejected,失败与被拒两类事件都收——同一队列可挂多条契约,叠加生效。变式二:契约下线。若风控组整体迁移到新交换机,旧绑定解绑后队列若无消费者也一并删除,避免留下"幽灵队列"持续接收无人认领的消息。
路由键是全系统的公共语言,散漫命名迟早酿成事故。实践中稳定的做法是三级结构"业务域.对象.事件":order.coupon.used、inventory.sku.depleted、payment.invoice.issued。配套三条纪律:
| 纪律 | 理由 |
|---|---|
| 全小写、点号分层、禁用空格 | topic 匹配以点号切词,混入空格必然漏判 |
| 只用字母数字与点号 | 路由键不承担数据职责,复杂信息放属性或载荷 |
| 事件用过去时 | 消息是"已发生的事实",不是"请执行的命令" |
第三条值得展开:order.created 描述事实,至于谁来响应、怎么响应,是消费者的事。一旦路由键写成动词命令(比如 do.refund),发布方就重新背上了对消费方行为的假设,解耦名存实亡。契约精神从命名开始。
命名规范落地时还有一个助推技巧:把规范做成代码检查。路由键在代码里通常是常量或枚举,用静态检查拦住不符合三级结构的字面量,比在评审会上人肉把关可靠得多。规范一旦依赖人的自觉,就会随团队扩张而稀释——机器守得住的边界,别交给自觉。
⚠️ 常见坑:把路由键当数据通道。见过团队把用户 ID 拼进路由键(
order.user.8848.created),想按用户过滤。结果绑定爆炸成千上万条,管理界面彻底失控。按用户维度的过滤应在消费端做,路由键的基数量级必须可控。
契约写好了,消息的最终归宿——队列——还剩最后一块拼图。下一节看队列声明时那些决定命运的参数。