Redis 如何实现发布、订阅?

Redis 如何实现发布、订阅?Redis 如何实现发布、订阅?

Redis 的发布订阅是一种消息通信模式,它实现了简单的消息队列功能,允许消息在发送者和接收者之间进行解耦。发送者(发布者)将消息发布到指定的频道,而接收者(订阅者)则可以订阅一个或多个频道来接收感兴趣的消息。

一、 核心命令与工作机制

Redis 提供了一系列简单的命令来实现发布订阅。

1. 订阅频道

订阅者使用 SUBSCRIBE 命令监听一个或多个频道。

# 订阅单个频道
SUBSCRIBE news

# 订阅多个频道
SUBSCRIBE news sports technology

执行 SUBSCRIBE 后,客户端会进入订阅状态,连接将专用于接收消息,此时不能执行其他非 "订阅" 系列的命令。

2. 发布消息

发布者使用 PUBLISH 命令向指定频道发送消息。

PUBLISH news "Redis 6.0 introduces threaded I/O!"

3. 按模式订阅

为了订阅多个符合特定模式的频道,可以使用 PSUBSCRIBE 命令。它支持通配符:

  • * :匹配任意数量的字符;
  • ? :匹配一个字符;
  • [ae]: 匹配括号内的字符;
# 订阅所有以 "logs:" 开头的频道
PSUBSCRIBE logs:*

当一个消息发布到 logs:error 时,所有通过 PSUBSCRIBE logs:* 订阅的客户端都会收到这条消息。

4. 退订

使用 UNSUBSCRIBEPUNSUBSCRIBE 命令来退订指定的频道或模式。

UNSUBSCRIBE news

二、 底层实现原理

再来说说其底层实现。Redis 服务器内部维护了两个主要的字典:

  1. pubsub_channels 字典:
    • Key: 频道名称(例如 "news", "sports")。
    • Value: 一个链表,链表中保存了所有订阅了这个频道的客户端。
  2. pubsub_patterns 链表:
    • 这个链表保存了所有模式订阅。
    • 每个节点包含:
      • 被订阅的模式(例如 "logs:*")。
      • 订阅这个模式的客户端。

工作流程:

当执行 PUBLISH news "hello" 时,Redis 会:

  1. 查找频道订阅者:pubsub_channels 字典中查找键为 "news" 的链表,然后遍历这个链表,将消息 "hello" 发送给链表中的每一个客户端。
  2. 查找模式订阅者: 遍历 pubsub_patterns 链表,检查频道名 "news" 是否匹配其中的某个模式(例如,如果存在模式 "n*",那么 "news" 就匹配)。对于每一个匹配的模式,将消息发送给对应的客户端。

三、优势与致命缺陷

Redis 发布订阅非常轻量、快速,但它是一个纯粹的内存操作、非持久化的消息系统。这既是它的优点,也是它在生产环境中作为核心消息队列的致命弱点。

优势:

  • 极致的轻量与高性能: 由于直接在内存中操作,没有磁盘 I/O 和复杂的持久化机制,消息传递速度极快。
  • 实现简单: API 非常简单,几行代码就能建立起一个消息通道。
  • 完美的解耦: 发布者和订阅者不需要知道彼此的存在。

致命缺陷与生产环境陷阱:

  1. 消息无持久化
    • 问题: Redis 不会对发布的消息进行任何形式的持久化。如果消息被发布时,没有任何订阅者在线,那么这条消息将永远丢失。
    • 场景: 订阅者客户端因为网络抖动、重启或崩溃而断开连接,在它重连期间的所有消息都无法收到。
  2. 无消息积压能力
    • 问题: Redis 不会为频道维护一个消息队列。它只负责将消息实时地分发给当前在线的订阅者。它不具备任何消息回溯或积压能力。
    • 场景: 如果生产者速度远大于消费者的处理速度,慢的消费者会直接丢失消息,因为消息不会被缓存起来。
  3. 消费者负载均衡问题
    • 问题:SUBSCRIBE 模式下,一条消息会被发送给所有订阅了该频道的消费者。这被称为 "广播" 模式。你无法让多个消费者共同消费一个频道的消息来实现负载均衡。
    • 场景: 如果你有 3 个消费者进程来处理 image_upload 频道的消息,希望它们轮流处理以减轻压力,原生的发布订阅无法做到。一条上传消息会被 3 个进程同时收到,导致重复处理。

四、 生产环境选型与实践建议

正因为上述缺陷,Redis 的原生发布订阅绝不应该用于传输重要的、不可丢失的业务消息。

1. 适用场景

  • 实时状态广播: 如在线聊天室、游戏内公告、服务器状态通知。这些消息本身是瞬态的,丢失一两条无关紧要。
  • 轻量级的配置更新: 通知所有服务实例,某个配置项已更新,需要重新拉取。即使错过通知,服务在下次拉取配置时也能纠正。
  • 应用内的事件总线: 在微服务内部,作为不同模块之间的事件传递机制。

2. 不适用场景

  • 订单处理、支付通知等关键业务消息。
  • 需要保证消息必达的通信。
  • 需要消峰填谷的异步任务队列。

3. 生产级替代方案

当你的场景需要可靠性时,请考虑以下方案:

  • Redis Streams (Redis 5.0+): 这是 Redis 官方推出的可靠的、支持持久化和积压的消息队列数据结构。它支持消费者组,可以实现真正的负载均衡和至少一次消费语义。这是取代发布订阅用于重要业务的首选 Redis 方案。
  • 专业的消息中间件: 如 Apache Kafka (高吞吐、持久化)、RabbitMQ (功能丰富、协议完善)、Apache Pulsar 等。它们提供了更强大的持久化、事务、消息确认和路由机制。

总结

面试官,对于 Redis 发布订阅,我的核心结论是:

Redis 发布订阅是一个优秀的"消息通知"机制,但它是一个糟糕的"消息队列"。

它通过内存中的 pubsub_channelspubsub_patterns 结构,以极高的效率实现了消息的实时广播。然而,其无持久化、无积压、纯广播的特性,决定了它只能用于可容忍消息丢失的非关键、实时性场景。

在技术选型时,如果业务要求消息的可靠性、持久化或消费者负载均衡,我会毫不犹豫地选择 Redis Streams 或专业的消息中间件,而不是原生的发布订阅。