Redis 如何实现发布、订阅?
结论:Redis 的发布订阅是**"广播 + 即时丢弃"**模型:
PUBLISH把消息推给当前在线的所有订阅者,SUBSCRIBE/PSUBSCRIBE按频道或模式订阅。它不持久化、不确认、不重放,客户端断线期间的消息永久丢失。因此它适合配置变更通知、缓存失效广播这类"丢了也能自愈"的场景,不适合当业务消息队列。
一、命令与语义
| 命令 | 作用 | 注意点 |
|---|---|---|
SUBSCRIBE ch1 ch2 | 订阅一个或多个频道 | 进入订阅态后只能执行订阅类命令 |
PSUBSCRIBE news.* | 按模式订阅 | 模式匹配开销随订阅者数量上升 |
PUBLISH ch msg | 向频道发消息 | 返回值为"当时收到消息的订阅者数量" |
UNSUBSCRIBE / PUNSUBSCRIBE | 退订 | 退订后回到普通命令模式 |
PUBSUB CHANNELS | 查看活跃频道 | 排查"到底有没有人订阅" |
PUBSUB NUMSUB ch | 查看某频道订阅者数 | 0 表示消息发出去无人接收 |
PUBSUB NUMPAT | 模式订阅总数 | 数量大时 PUBLISH 会变慢 |
订阅态的客户端不能执行 GET、SET 等命令,因此生产上一般用独立连接做订阅,业务连接只做普通读写。
二、投递模型与边界
┌──────────────┐
PUBLISH news │ Redis 主节点 │
──────────────► │ 频道字典 │
│ news → [C1,C2]│
└──────┬───────┘
│ 立即写入各订阅者连接缓冲区
┌─────────────┼─────────────┐
▼ ▼ ▼
C1(在线) C2(在线) C3(已断线)
收到 收到 收不到,消息直接丢弃
关键事实:
1. 没有 ack,没有重投,没有 pending 列表
2. 断线重连后只能收到"重连之后"的消息
3. 主从切换 / 集群中,消息只在当前节点广播,不跨节点转发
由此带来三个必须知道的限制:
- 不可靠:网络抖动、客户端重启、慢消费者(输出缓冲区溢出被踢)都会丢消息;
- 不持久:Redis 重启后频道订阅关系与消息全部消失;
- 不跨分片:Cluster 模式下
PUBLISH只发给当前节点的订阅者,客户端要么订阅所有节点,要么借助SSUBSCRIBE(7.0 的分片订阅,仍需按槽路由)。
三、典型用法与代码
# 终端 A:订阅
redis-cli SUBSCRIBE cache:evict
# 终端 B:发布,返回 1 表示有 1 个订阅者收到
redis-cli PUBLISH cache:evict "user:1001"
# 键空间通知:监听所有 key 的过期事件(需先开启配置)
redis-cli CONFIG SET notify-keyspace-events Ex
redis-cli PSUBSCRIBE '__keyevent@0__:expired'
// Spring Data Redis:用独立容器做订阅,避免占用业务连接
@Configuration
public class SubConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory f,
MessageListener listener) {
RedisMessageListenerContainer c = new RedisMessageListenerContainer();
c.setConnectionFactory(f);
c.addMessageListener(listener, new PatternTopic("cache:evict"));
c.setTaskExecutor(Executors.newFixedThreadPool(2)); // 慢消费者会拖住其它频道
return c;
}
}
消费端要做幂等与兜底:收到失效消息只是"尽快删本地缓存",即使消息丢了,本地缓存也会因 TTL 到期而收敛。
四、常见追问
- 问:发布订阅和 Stream 的本质区别?答:Pub/Sub 是"在线即达、离线即丢"的推送;Stream 是"追加日志 + 消费组 + ack",支持重放与积压,可靠性完全不同。
- 问:订阅者很多时 PUBLISH 会慢吗?答:会。PUBLISH 的复杂度与订阅者数量相关,且模式订阅需要逐个匹配;上万订阅者时延迟明显,应改用 Stream 或消息队列。
- 问:为什么订阅态不能执行普通命令?答:协议层面订阅态只允许订阅/退订/
PING/QUIT,否则响应会混入推送消息导致客户端解析错乱。 - 问:Cluster 下怎么让所有节点都收到消息?答:向每个主节点分别 PUBLISH,或使用 7.0 的
SSUBSCRIBE按槽订阅;更常见的做法是换成 Stream 或外部 MQ。 - 问:键空间通知能当延迟队列用吗?答:不推荐。过期事件是"删除发生时"才发出,Redis 不保证准时,且事件不持久化,重启或主从切换都可能丢事件。
- 问:怎么排查"消息发了但没人收到"?答:
PUBSUB CHANNELS看频道是否存在、PUBSUB NUMSUB看订阅者数、查客户端输出缓冲区client-output-buffer-limit pubsub是否触发断连。
小结
- Pub/Sub 是轻量广播,语义是"尽力而为",不要向它要可靠性。
- 适用场景:配置热更新通知、本地缓存失效、监控指令下发;不适用:订单、支付等不可丢消息。
- 需要可靠投递就上 Stream(有 ack 与 pending)或专业 MQ,Pub/Sub 只做提速的那一层。