Redis 如何实现发布、订阅?

#中间件#面试八股

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 只做提速的那一层。
笔记加载中…