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 # 退订所有消息格式是多行数组:第一个字段是消息类型(subscribe、message、unsubscribe),第二个是频道名,第三个是消息内容(或订阅数)。订阅者会一直阻塞,直到按 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:info、log:warn、log: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. 应用场景
- 多实例间缓存失效广播:Web 服务部署多台,任意一台更新了数据库,通过 Pub/Sub 通知其他台删 Redis 缓存(失效广播)。
- 配置热更新:配置变更时 PUBLISH 一次,所有服务订阅收到立即生效,无需重启。
- 实时聊天/IM 在线消息:用户在线时收到,掉线时不收(符合 IM 的语义)。
- 分布式任务调度:任一台抢到任务,广播通知其他台别再抢。
- 监控大屏实时数据:服务把指标 PUBLISH 出去,前端订阅渲染图表。
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 事务 →