大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Pub/Sub 消息:判题完成的实时广播
这一课你将能做到
- 使用订阅、发布、模式订阅完成一条消息的实时广播
- 为本站设计「判题完成 → 网页实时弹出结果」的通知链路
- 说清楚 Pub/Sub 与 Stream、List 在消息可靠性上的根本差异
为什么要学
用户在题库提交代码后,页面会进入「判题中……」状态。判题可能要几秒到几十秒,前端怎么知道判完了?两种笨办法:页面每隔 1 秒问一次服务器「好了没」(轮询,浪费请求),或者一直傻等。
更优雅的方式是发布/订阅(Publish/Subscribe):判题服务完成后往一个频道(channel)「喊一嗓子」,所有订阅了这个频道的连接立刻收到消息,网页瞬间更新。它特别适合「一对多、瞬时、不需要留存」的实时通知。
先建立图像
发布者 PUBLISH judge:result "{...}"
│
┌───────────┼───────────┐
▼ ▼ ▼
订阅者A 订阅者B 订阅者C ← 同一条消息,人人有份
(用户1001 (数据统计 (管理员
的网页) 服务) 控制台)
关键:消息「发完即散」。发布那一刻没人订阅,消息就直接消失,
Redis 不会帮你存——这是和 Stream 最大的不同。
跟着做
步骤 1:订阅频道
终端 A(代表用户 1001 的网页服务端连接):
127.0.0.1:6379> SUBSCRIBE judge:result:1001
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "judge:result:1001"
3) (integer) 1
订阅后该连接进入订阅模式,只能收消息,不能执行普通命令(可用 SUBSCRIBE/UNSUBSCRIBE/PSUBSCRIBE/PING)。
步骤 2:发布消息
终端 B(判题服务):
127.0.0.1:6379> PUBLISH judge:result:1001 "{\"submissionId\":8801,\"verdict\":\"AC\",\"score\":100}"
(integer) 1
返回值 1 表示当前有 1 个订阅者收到了消息。终端 A 立刻显示:
1) "message"
2) "judge:result:1001"
3) "{\"submissionId\":8801,\"verdict\":\"AC\",\"score\":100}"
步骤 3:模式订阅一次监听一批频道
管理员控制台想监听所有用户的判题结果,用 PSUBSCRIBE 配合通配符:
127.0.0.1:6379> PSUBSCRIBE judge:result:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "judge:result:*"
3) (integer) 1
判题服务再发一条:
127.0.0.1:6379> PUBLISH judge:result:1002 "{\"verdict\":\"WA\"}"
(integer) 1
管理端收到时会额外告诉你匹配的是哪个模式:
1) "pmessage"
2) "judge:result:*"
3) "judge:result:1002"
4) "{\"verdict\":\"WA\"}"
通配符支持 *(任意字符)、?(单字符)、[abc](字符集合)。比如 judge:result:100? 匹配 1000-1009。
步骤 4:退订与查看频道
127.0.0.1:6379> UNSUBSCRIBE judge:result:1001
127.0.0.1:6379> PUNSUBSCRIBE judge:result:*
另开一个普通连接,可以看当前有哪些频道有人订阅、各有多少订阅者:
127.0.0.1:6379> PUBSUB CHANNELS
1) "judge:result:1001"
127.0.0.1:6379> PUBSUB NUMSUB judge:result:1001
1) "judge:result:1001"
2) (integer) 1
127.0.0.1:6379> PUBSUB NUMPAT
(integer) 0
NUMPAT 统计的是模式订阅的数量。
步骤 5:搭建本站完整通知链路
设计频道为每个用户独占,避免串消息:
judge:result:{userId}
用户提交代码(submissionId 8801)后,前端建立监听。在真实网站里通常不是浏览器直连 Redis,而是通过 WebSocket 桥接:
浏览器 ──WebSocket──> 网站后端 ──SUBSCRIBE──> Redis
判题服务 ──PUBLISH──> Redis ──> 网站后端 ──WebSocket──> 浏览器弹出「通过!+100 分」
判题服务执行:
127.0.0.1:6379> PUBLISH judge:result:1001 "{\"submissionId\":8801,\"verdict\":\"AC\",\"score\":100}"
(integer) 1
前端收到后还可以做两件事:请求刷新排行榜(第 6 课的 ZSET 已更新)、更新做题进度(第 5 课的 Hash 已更新)。Pub/Sub 只负责「喊一声该刷新了」,真正的数据仍然从各结构里读。
判题服务也可以按题目广播「大厅消息」,所有正在看这道题讲解的用户都收到「刚才有 32 人通过此题」:
127.0.0.1:6379> PUBLISH problem:501:live "{\"acCount\":32}"
(integer) 12
步骤 6:验证「消息不落地」
先在终端 B 发布一条「没人订阅」的消息:
127.0.0.1:6379> PUBLISH nobody:home hello
(integer) 0
返回 0。再去终端 A 订阅 nobody:home——你永远收不到那条 hello,它已经蒸发了。这个实验要亲手做一次,能帮你记住 Pub/Sub 的适用边界。
讲清楚要点
- Pub/Sub 是瞬时的扇出(fan-out)通知:一对多、不存储、不确认,擅长「此刻在线的人立刻知道」。
- 发布者不关心有没有人收:返回值只是当前订阅者数量,消息对离线订阅者直接丢失。
- 订阅连接会断线:网络抖动期间的消息全部错过,重连后没有补偿,所以重要数据不能只靠 Pub/Sub 传递。
- 与其他结构的分工:List/Stream 解决「任务/消息要被可靠处理」;Pub/Sub 解决「在线者实时被告知」。本站实践中常组合使用:数据落 ZSET/Hash,事件用 Pub/Sub 广播;或事件本身进 Stream,再由服务把结果 Pub 给在线用户。
- 慢消费者有风险:如果订阅端处理太慢,Redis 的客户端输出缓冲区会堆积,超限后 Redis 会主动断开该订阅者,相关配置为
client-output-buffer-limit pubsub。 - 频道名是普通字符串:同样遵循命名规范,按业务分层;模式订阅很方便,但模式过宽(如
*)会让该连接收到全库噪音。
你可能会懵的地方
-
订阅之后为什么打别的命令没反应? 连接处于订阅模式,只接受订阅相关命令。另开一个连接执行普通操作即可。
-
Pub/Sub 消息会持久化吗?重启还在吗? 不会,完全不落地,重启、断线、无人订阅都会丢。它是「烟火信号」不是「信箱」。
-
为什么不用 Pub/Sub 做判题任务队列? 判题机离线时任务全部丢失,且一条消息会发给所有订阅者(无法像队列那样一条任务只被一台机器处理)。任务排队请用 List/Stream,Pub/Sub 只用于通知结果。
-
PUBLISH 的返回值有什么用? 表示投递到了几个订阅者(含模式订阅)。返回 0 说明无人接收,发布方可以据此决定要不要记日志,但消息本身无法重发。
-
Sharding 集群下 PUBLISH 还能广播全集群吗? 普通频道消息会在集群节点间转发(有额外开销);带分片属性的
SPUBLISH/SSUBSCRIBE(Redis 7)只在同一槽相关节点内传播,大规模场景更省。初学阶段先掌握普通 Pub/Sub。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 用 Pub/Sub 派发判题任务 | 消费者离线即丢、无法确认 | 任务用 List/Stream,结果再 Pub |
| 指望离线用户上线后补收消息 | 消息不存储 | 重要通知落 Stream 或站内信 |
一个订阅 PSUBSCRIBE * | 收到全库消息,处理不过来 | 模式尽量精确 |
| 订阅端在收消息线程里做重活 | 输出缓冲堆积被踢断 | 收到即转交后台线程处理 |
| 用户共用一个频道 | A 能收到 B 的判题结果 | 频道按用户隔离 |
| 断线不做重连与状态对账 | 错过消息且不自知 | 自动重连,重连后主动查一次最新状态 |
卡点急救
- 订阅了却收不到消息:核对频道名是否完全一致(模式匹配也检查),用 PUBSUB CHANNELS 确认订阅关系确实存在。
- 订阅连接频繁断开:多半是消费太慢触发输出缓冲区限制,减轻单条处理逻辑,或让应用及时读取。
- 前端一直停在「判题中」:可能错过 PUBLISH(订阅建立慢了一拍)。兜底方案:前端订阅同时保留一次定时状态查询作为补偿,收到结果即取消。
面试常问
-
Redis Pub/Sub 的特点和适用场景? 发布订阅、一对多、消息不持久化、离线即丢失、无消费确认。适合在线实时通知:结果播报、配置刷新广播、大屏推送;不适合任务队列和可靠投递。
-
Pub/Sub 和 Stream 有什么区别? Pub/Sub 发完即散,只服务在线订阅者;Stream 是持久化日志,消息可留存、可回溯,有消费组和确认。实时在线通知选 Pub/Sub,可靠异步处理选 Stream,也可组合使用。
-
订阅者断线重连后能收到漏掉的消息吗? 不能。需要补偿就重连后主动查询当前状态,或让关键事件同时写入 Stream。
自测三问
- 写出「订阅频道 → 发布消息 → 模式订阅 → 退订」各一条命令。
- 无人订阅时 PUBLISH 的消息去哪了?这决定了它不能用来做什么?
- 本站判题场景中,任务派发和结果通知分别应该用什么结构?为什么?
小结一句话
Pub/Sub 是不落地的一对多实时广播,用它做「判完了」的即时通知最顺手,但只要涉及留存和可靠,就必须交给 Stream。
延伸
- Pub/Sub 文档
- 分片 Pub/Sub(Redis 7)
- 下一课:事务与 Lua——把多条命令捆成原子操作,写出可靠的分布式锁。