Redis 不只是存储,还内建了消息传递能力。本节拆它的两代消息机制——经典的发布/订阅(Pub/Sub)与 5.0 引入的 Stream——重点讲清前者"不落盘、无确认"的语义边界,以及什么场景该用 Stream、什么场景该上专业消息队列。这是 2.1 节"值不可知"之外的另一条能力线:Redis 对消息的处理同样是特化的。
Pub/Sub 的模型极简:订阅者订阅频道(channel),发布者向频道发消息,所有在线订阅者实时收到。它的关键语义有三条,每条都是使用边界的来源:
SUBSCRIBE alerts:order # 订阅者上线并订阅频道 PUBLISH alerts:order "ORDER-9001 已发货" # 发布者投递 # 所有当前订阅 alerts:order 的客户端都会实时收到
一个容易混淆的功能是键空间通知:开启后 Redis 会把"某键被修改/过期"作为事件发布到约定频道,实现"键过期即触发回调"——5.4 延迟队列的进阶版常靠它。它同样继承 Pub/Sub 的不落盘语义,错过就是错过。
Stream 是为补齐 Pub/Sub 短板而生的:消息按序追加进流、持久存在、每条有唯一 ID,支持消费者组(一个组内多消费者竞争消费、各自记录消费位点)、确认(XACK)与未确认重投(XAUTOCLAIM)——已经是轻量级消息队列的完整形态:
XADD orders * sku 88312 qty 1 # 追加消息,* 表示自动生成 ID XGROUP CREATE orders g1 0 # 建消费者组 g1 XREADGROUP GROUP g1 c1 COUNT 10 STREAMS orders > # 消费者 c1 取 10 条 XACK orders g1 1693-1 # 处理完成后确认
| 能力 | Pub/Sub | Stream |
|---|---|---|
| 离线可收 | 否(错过即失) | 是(消息持久保留) |
| 消费确认与重投 | 无 | 有(XACK、待认领列表) |
| 竞争消费 | 无(广播) | 有(消费者组分摊) |
| 适用 | 实时通知、配置广播 | 轻量队列、事件溯源 |
背景:客服系统要给坐席推送"新工单到达"提醒,初期用 Pub/Sub 实现:工单服务发布,各坐席终端的网关进程订阅并推送到浏览器。
操作:运行两个月后三类问题暴露:坐席网关发布重启的窗口内漏掉新工单提醒(不落盘的代价);工单高峰期提醒与终端心跳消息混杂,无法区分优先级;漏掉的提醒无据可查,客服与产品各执一词。改造:提醒改走 Stream——每类工单一个流,网关用消费者组消费、处理成功后 XACK;网关重启后从上次消费位点续读,不再漏单;消息保留 7 天(MAXLEN 裁剪),争议可回查。
结果:提醒到达率从约 97% 提升到 100%(可观测);漏单争议从每周数起降为零;网关重启不再需要"人工对一遍工单"。
解读:演进的分水岭就是一句话——消息丢得起用 Pub/Sub,丢不起用 Stream。提醒丢一条是客诉,库存事件丢一条是资损,定级决定选型。同时注意 Stream 的保留策略:MAXLEN 裁剪要设,不然流会无限增长吃掉内存(与 5.4 的淘汰策略一脉相承——内存数据库的容量纪律)。
变式:若消息量上到每秒数万且要求强顺序分区、事务消息、死信队列等完整语义,Stream 的轻量设计开始吃力,应迁往专业消息队列(Kafka/RocketMQ 一类)。Redis 消息能力的定位是"顺手解决中小规模",不是替代消息中间件。
第一个是用 Pub/Sub 承担关键消息:它最常见也最致命的误用——重启丢消息是设计语义不是 bug,关键链路必须 Stream 或专业队列。第二个是消费者组位点管理误用:处理失败不 XACK 却也不重投,消息滞留在待认领列表无人问津;要有定期 XAUTOCLAIM 或监控待认领数量的任务。第三个是无 MAXLEN 的流:长期运行后内存被流吃光触发淘汰(若策略是 allkeys 系,连缓存键都遭殃),任何流都要配容量上限。第四个是用 Pub/Sub 做集群广播时的语义误解:集群模式下 Pub/Sub 的订阅是节点级广播,发布到任意节点全体可见,但 Stream 的消费者组是键级路由(按槽位),两者的拓扑行为不同,混用会踩坑。
Redis 有两种消息能力,常被混为一谈,实际定位完全不同。
| 维度 | Pub/Sub | Streams(5.0 起) |
|---|---|---|
| 消息是否持久化 | 不持久化,订阅者离线则消息丢失 | 持久化,可重复消费 |
| 消费模型 | 广播,所有订阅者都收到同一条 | 消费组,一条消息只被组内一个消费者处理 |
| 是否支持确认 | 不支持 | 支持 ACK,未确认的消息可重新投递 |
| 是否支持回溯 | 不支持 | 支持按 ID 重读历史 |
| 适用 | 实时通知、在线状态广播 | 消息队列、事件溯源、可靠异步任务 |
# Pub/Sub:广播型,订阅者必须在线 SUBSCRIBE news:flash # 订阅频道 PUBLISH news:flash "Redis 7.0 发布" # 发布消息 # Streams:可靠队列,支持消费组与确认 XADD events * type "order_created" order_id 90001 XGROUP CREATE events cg_order 0 # 创建消费组 XREADGROUP GROUP cg_order consumer-1 COUNT 10 BLOCK 2000 STREAMS events > # 处理完成后确认,未确认的消息会在该消费者故障时重新分配 XACK events cg_order 1690000000000-0
需求:订单创建后需要触发三个异步动作(发券、通知、更新统计),要求消息不丢、可重试。
XADD 一条事件,拿到消息 ID;XACK;处理失败不确认,消息留在待处理列表(PEL)里;XAUTOCLAIM 把它的待处理消息转给其他消费者;XLEN 看队列长度、XPENDING 看待处理消息数与空闲时长,超阈值告警。这套模型已经具备主流消息队列的核心语义(至少一次投递、消费确认、故障转移),适合中等吞吐、不希望引入额外中间件的场景。
用 Pub/Sub 的场景:消息丢了无所谓、订阅者必须在线、需要低延迟广播——比如在线用户状态同步、实时推送通知、聊天室。
用 Streams 的场景:消息不能丢、需要消费确认与重试、需要回溯历史——比如订单事件、异步任务队列。
两者都不合适的场景:需要严格的 exactly-once 语义、需要复杂路由与死信队列、或者吞吐要求达到每秒数十万级。此时应当选择专业的消息中间件,Redis 的定位是"够用且轻量",不是"替代 Kafka 或 RabbitMQ"。
消息讲完,下一节回到正确性主战场:多步操作的原子性——事务与 Lua 脚本。