4.3 发布订阅:一张订阅者链表 本节摘要:发布订阅在服务端维护"频道到订阅者列表"的反向索引,PUBLISH 时遍历列表即时推送。它解耦的是"谁关心这件事",不保证送达、不落盘、离线收不到——理解这个边界,才知道何时用它、何时换 Stream。 结构有多简单 订阅不是轮询。客户端执行 SUBSCRIBE 后,服务端在内存里给该频道挂上这个客户端的引用;PUBLISH 一到,遍历挂载列表逐个写响应——就是 2.3 里那张 dict,键是频道名,值是订阅者链表。
本节摘要:发布订阅在服务端维护"频道到订阅者列表"的反向索引,PUBLISH 时遍历列表即时推送。它解耦的是"谁关心这件事",不保证送达、不落盘、离线收不到——理解这个边界,才知道何时用它、何时换 Stream。
订阅不是轮询。客户端执行 SUBSCRIBE 后,服务端在内存里给该频道挂上这个客户端的引用;PUBLISH 一到,遍历挂载列表逐个写响应——就是 2.3 里那张 dict,键是频道名,值是订阅者链表。
# 终端一 > SUBSCRIBE order:events subscribe order:events 1 # 终端二 > PUBLISH order:events "订单1001已支付" (integer) 1 # 返回送达的订阅者数 # 终端一立即收到 message order:events 订单1001已支付
订阅态的连接有几个规矩要立好:SUBSCRIBE 之后这条连接进入订阅模式,只能再发 SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE、PING 这几类命令,发 SET 会直接报错——所以订阅连接要与命令连接分开,各司其职。订阅确认那条消息的第三个数字是"当前连接已订阅的频道数",断线重连后拿它核对订阅是否补齐,是消费端自检的常用手段。PING 在订阅态也能用且服务端会照常回 PONG,长连接的保活探活靠它。
交付语义再强调一遍:PUBLISH 返回的 1 只表示"当时有一个在线订阅者收到了推送",推送进对方的连接缓冲区后,对方进程崩没崩、处理没处理,发布方一概不知。把返回值当"处理成功"用,是发布订阅最常见的语义误用。
模式订阅 PSUBSCRIBE 用的是另一棵模式串的前缀树,order:* 能一次挂载所有以 order 冒号开头的频道。
模式订阅的完整会话走一遍。背景:风控服务想监听所有订单类事件,但订单事件按业务线拆成了几十个频道;操作:
# 终端一:按模式挂载 > PSUBSCRIBE order:* psubscribe order:* 1 # 终端二:往任意匹配频道发布 > PUBLISH order:paid "1001" (integer) 1 > PUBLISH order:refund "2002" (integer) 1 # 终端一各收到一条,报文类型是 pmessage,带模式与具体频道两个字段 pmessage order:* order:paid 1001 pmessage order:* order:refund 2002
解读:一条 PSUBSCRIBE 顶几十条 SUBSCRIBE,新增业务线频道无需改订阅代码;pmessage 报文比 message 多一个模式字段,客户端按具体频道字段做分发。变式与坑:模式与精确订阅可能同时命中同一条消息(既订阅 order:paid 又订阅 order:* 时收到两份),消费端要去重;模式串本身没有变量捕获能力,想把"哪个业务线"拆出来只能自己解析频道名字符串。监控订阅规模用 PUBSUB NUMPAT 看模式数、PUBSUB NUMSUB 看具体频道的订阅者数,巡检脚本留这两条就够。

发布订阅像广播喇叭,Stream 像录音机加分发表:
| 维度 | 发布订阅 | Stream |
|---|---|---|
| 离线消息 | 无,错过即丢 | 有,按 id 回放 |
| 确认机制 | 无 | XACK 加 PEL |
| 消费组 | 无 | 多组独立消费 |
| 落盘 | 不落 | 随持久化落盘 |
| 顺序 | 单频道内有序 | 全局按 id 有序 |
| 适合 | 实时通知、配置刷新、聊天室在场者 | 任务流、事件溯源 |
判据一句话:消息丢了要不要紧。要紧用 Stream;纯粹的"此刻在线者请刷新状态",发布订阅是最轻的答案。
分界线上还有个中间地带值得点名:聊天室的"在场成员列表"。成员进出事件丢了要紧(列表会失真),但也不值得为它上 Stream 的确认机制——惯用解法是把权威列表存进 Set,发布订阅只广播"列表版本变了",订阅者收到后拉一次全量列表自纠。丢了广播,下次进出时版本再变,仍能收敛回正确状态。
补一个版本演进的知识点:7.0 引入了分片订阅 SSUBSCRIBE 与 SPUBLISH。普通 PUBLISH 在集群里要把消息广播到所有节点(每个节点都可能挂着订阅者),分片订阅则把频道的归属固定到某个槽所在的节点,发布消息只走一个节点——集群内广播流量从"节点数倍"降为"一倍"。代价是分片频道必须带 hash tag、订阅者要连到正确的分片节点。老业务无感,新集群里高频广播的场景值得优先选它。
慢消费者的连锁反应也要认识:订阅方处理慢、连接缓冲区堆积,超过 client-output-buffer-limit 的 pubsub 限额时服务端会直接断开这条订阅连接——消息堵在缓冲区比丢掉更伤,服务端选择断链兜底。被断的消费者重连后错过的消息不会补发,现象就是"消费端莫名掉线、重启后少了一段事件"。定位这类问题看 INFO clients 的阻塞计数与断链日志,根因往往是下游处理能力不足,扩消费者比调缓冲区上限治本。
配置热更新是发布订阅的经典主场。广播"配置版本号变了",订阅者收到后自己去拉最新配置——只广播信号,不广播内容,消息体恒定一个小整数,即使丢一次,下次变更也会再触发拉取,自愈。
> SET config:version 17 > PUBLISH config:changed 17 # 订阅方收到后按版本号决定是否重拉
这套结构的稳健来自三重自愈:订阅方错过一次广播没关系,版本号摆在那,下次任何变更都会触发比对;发布方不需要知道多少人订阅、谁在线;消息体恒定是一个小整数,即使全集群广播,带宽也便宜。"只广播信号"这个习惯可以推广到所有用发布订阅的地方——凡是想广播"一坨数据"的冲动,都先问一句订阅方能不能自己拉。
⚠️ 常见坑:集群模式下 PUBLISH 会广播到所有节点(每个节点都可能挂着订阅者),高频大消息发布会放大集群内部流量。广播内容务必小,或者改走专业 MQ。