Redis 发布订阅(Pub/Sub)

Redis 自带一套发布订阅(Publish/Subscribe)机制——发送方往一个频道发消息,所有订阅了这个频道的客户端同时收到。这是聊天室、实时通知、配置广播的轻量方案。本篇讲它的用法、局限,以及 Redis 5.0 引入的替代方案 Stream

1. 基础用法:SUBSCRIBE / PUBLISH

需要两个终端演示:一个订阅,一个发布。打开两个 redis-cli 窗口:

# 终端 A:订阅频道(打开 redis-cli 后)
SUBSCRIBE news
# Reading messages... (press Ctrl-C to quit)
# 1) "subscribe"
# 2) "news"
# 3) (integer) 1

# 终端 B:向频道发消息
PUBLISH news "Redis 7.2 发布了"
# 返回收到消息的订阅者数:1

# 终端 A 此时收到:
# 1) "message"
# 2) "news"                          # 频道名
# 3) "Redis 7.2 发布了"               # 消息内容

# 一次订阅多个频道
SUBSCRIBE news sports tech

# 退订
UNSUBSCRIBE news              # 退订单个
UNSUBSCRIBE                   # 退订所有

消息格式是多行数组:第一个字段是消息类型(subscribemessageunsubscribe),第二个是频道名,第三个是消息内容(或订阅数)。订阅者会一直阻塞,直到按 Ctrl-C 退出。

2. 模式订阅:PSUBSCRIBE

实际项目里,订阅者往往关心"一类"频道而非单个。Redis 支持通配符模式订阅:

# 模式订阅(用通配符匹配多个频道)
PSUBSCRIBE log:*
# 匹配 log:error、log:info、log:warn 等所有以 log: 开头的频道

# 通配符:
# *  : 任意多个字符
# ?  : 单个字符
# [abc] : 字符集

# 发消息(会被上面的模式订阅收到)
PUBLISH log:error "DB connection failed"
PUBLISH log:info "User logged in"

# 模式订阅收到的消息格式有 4 个字段:
# 1) "pmessage"
# 2) "log:*"              # 匹配的模式
# 3) "log:error"          # 实际频道
# 4) "DB connection failed"

# 退订模式
PUNSUBSCRIBE log:*
PUNSUBSCRIBE               # 退订所有模式

模式订阅在日志系统特别有用——所有服务把日志发到 log:infolog:warnlog:error,日志收集器只需 PSUBSCRIBE log:* 就能收到所有。注意模式订阅的消息格式比普通订阅多一个字段(实际频道名),处理时要分支判断。

3. Node.js 聊天室实例

// Node.js 实现一个简单的聊天室
// 用 npm install ioredis
import Redis from "ioredis";

// 订阅者:专门用一个连接订阅
const sub = new Redis();
sub.subscribe("chat:room1");
sub.on("message", (channel, message) => {
  console.log("收到:", channel, message);
});

// 发布者:另一个连接发送
const pub = new Redis();
setInterval(() => {
  pub.publish("chat:room1", "你好,大家好!");
}, 5000);

// 注意:同一个连接不能既订阅又发布,
// 必须用两个独立的 Redis 连接实例。

关键点:一个连接不能既订阅又发布——所有 ioredis/jedis/go-redis 客户端都要求订阅用一个独立连接。这是因为 SUBSCRIBE 之后,这个连接进入"订阅模式",除了 SUBSCRIBE/UNSUBSCRIBE 等少数命令外,其他命令都会报错。所以聊天室通常每个客户端开两个连接:一个订阅、一个发布。

4. Pub/Sub 的关键限制

Pub/Sub 看起来很美,但有几个致命限制决定了它只能用于"轻量级"场景:

# Pub/Sub 的关键限制
# 1. 即时性:订阅之前发的消息不会收到
PUBLISH news "msg1"          # 此时还没人订阅,消息丢弃
SUBSCRIBE news               # 现在订阅,收不到 msg1

# 2. 断线丢失:订阅者断开期间的消息全部丢失
# 客户端重连后只能收"重连后"的消息

# 3. 无消费确认:不知道谁收了、谁没收
PUBLISH news "msg"           # 返回 1 表示"当前有 1 个订阅者"
# 但如果订阅者处理时崩溃,Redis 不知道,不会重发

# 4. 无持久化:消息不写入 RDB/AOF(只过内存)
# 重启 Redis 后历史消息全没

# 适用场景:
# - 实时广播:配置变更通知、多实例间缓存失效
# - 聊天室、实时通知、IM 在线消息
# - 监控告警:某 key 过期/修改时通知
# 不适用:订单、支付、消息队列(需要保证消费的)

总结成一句话:Pub/Sub 是"实时广播",不是"消息队列"。任何需要"消息不能丢、必须消费、能回溯"的场景,都不要用 Pub/Sub——要用 Stream(或外部的 RabbitMQ/Kafka)。

5. 应用场景

6. 键空间通知

Redis 还能用 Pub/Sub 机制做一件特别的事——订阅 key 的变化事件。比如某个 key 过期、被删、被改时,自动收到通知。

# 键空间通知(Keyspace Notification)
# 当某个 key 发生变化(改、删、过期)时,Redis 会发一条 Pub/Sub 消息

# 配置开启(默认关闭,因为消耗 CPU)
CONFIG SET notify-keyspace-events KEA
# K = Keyspace 事件, E = Keyevent 事件
# g = generic 通用命令(DEL/EXPIRE 等)
# $ = string 命令, l = list, h = hash, ...
# A = alias for "g$lshzxe"全部

# 订阅某个 key 的事件(以 __keyspace@<db>__ 为前缀)
SUBSCRIBE __keyspace@0__:session:user1
# 当 session:user1 被 SET/DEL/EXPIRE 时会收到通知

# 订阅某类事件(以 __keyevent@<db>__ 为前缀)
SUBSCRIBE __keyevent@0__:expired
# 收到所有"过期"事件,消息内容是过期的 key 名

# 典型场景:
# - "key 过期时通知我"做延迟任务(不如 zset 延迟队列可靠)
# - 监控某些重要 key 的修改
# 注意:过期事件不保证准时,Redis 是惰性删除+定期删除,
# 实际触发可能晚几分钟,不能用于精确延迟

"key 过期事件"是高频面试题,但要注意它的不精确性:Redis 用惰性删除 + 定期删除,过期 key 可能要等到下次被访问或定期扫描时才真正删掉,过期事件也才在那个时刻发出——可能延迟几秒到几分钟。所以"用 keyspace 通知做精确延迟任务"是不可靠的,精确延迟请用 zset 方案(见 sorted-set 篇)。

7. Stream:可持久化的消息队列

为了解决 Pub/Sub 的局限,Redis 5.0 引入了 Stream 数据类型——一个持久化、支持消费组、可回溯的消息队列,模型类似 Kafka。

# Redis 5.0+ 引入的 Stream:持久化消息队列(类似 Kafka)
# 解决了 Pub/Sub 的"无持久化、无消费组"问题

# 生产者写入
XADD orders * id 1001 amount 99.5
# * 表示自动生成消息 ID(时间戳-序号)
# 后面是 field-value 对(类似 hash)

# 返回消息 ID
"1700000000000-0"

# 消费者读(范围或阻塞)
XRANGE orders - +                   # 读全部
XRANGE orders 1700000000000-0 +     # 从某 ID 开始
XREAD COUNT 10 BLOCK 5000 STREAMS orders $    # 阻塞读新消息

# 消费组(Consumer Group):多消费者分担消息
XGROUP CREATE orders group1 0
# 让 group1 的 consumer1 读一条
XREADGROUP GROUP group1 consumer1 COUNT 1 STREAMS orders >
# 确认消费完成
XACK orders group1 <message-id>

# Streams 的优点(对比 Pub/Sub):
# - 消息持久化(写入 AOF/RDB,重启不丢)
# - 支持消费组、消费确认、回溯
# - 支持积压处理(PEL)
# - 类似 Kafka 的模型,但单机版,无分片
# 适合:订单、支付、需要"至少消费一次"的业务

Stream 是 Redis 自带的"轻量级 Kafka":单机版、无分片、消息可持久化、支持消费组和确认机制。比起引入 Kafka 这种重型组件,Stream 在中小项目里几乎能满足所有"消息队列"需求。它的命令以 X 开头(XADD/XREAD/XGROUP/XACK),核心概念和 Kafka 类似——生产者写入 stream、消费组分担消费、ACK 确认完成、PEL 记录未确认消息。

何时仍要用 Kafka?需要水平分片(单 Redis 实例扛不住吞吐)、多机房复制长消息保留(几个月)——这些 Stream 做不到。Stream 适合"单机十万级 QPS、消息保留几天到几周"的场景。

小结

Pub/Sub 是实时广播:简单、低延迟,但不持久、不保证消费、断线丢失,适合聊天室、配置广播。需要可靠消息队列请用 Stream(持久化、消费组、ACK)。键空间通知是 Pub/Sub 的一个特殊应用,但不要依赖它做精确延迟。下一篇讲 Redis 的事务机制——MULTI/EXEC。

← 上一篇 Redis 持久化

下一篇 Redis 事务

✈️💬