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. 退订
使用 UNSUBSCRIBE 和 PUNSUBSCRIBE 命令来退订指定的频道或模式。
UNSUBSCRIBE news
二、 底层实现原理
再来说说其底层实现。Redis 服务器内部维护了两个主要的字典:
pubsub_channels字典:- Key: 频道名称(例如
"news","sports")。 - Value: 一个链表,链表中保存了所有订阅了这个频道的客户端。
- Key: 频道名称(例如
pubsub_patterns链表:- 这个链表保存了所有模式订阅。
- 每个节点包含:
- 被订阅的模式(例如
"logs:*")。 - 订阅这个模式的客户端。
- 被订阅的模式(例如
工作流程:
当执行 PUBLISH news "hello" 时,Redis 会:
- 查找频道订阅者: 在
pubsub_channels字典中查找键为"news"的链表,然后遍历这个链表,将消息"hello"发送给链表中的每一个客户端。 - 查找模式订阅者: 遍历
pubsub_patterns链表,检查频道名"news"是否匹配其中的某个模式(例如,如果存在模式"n*",那么"news"就匹配)。对于每一个匹配的模式,将消息发送给对应的客户端。
三、优势与致命缺陷
Redis 发布订阅非常轻量、快速,但它是一个纯粹的内存操作、非持久化的消息系统。这既是它的优点,也是它在生产环境中作为核心消息队列的致命弱点。
优势:
- 极致的轻量与高性能: 由于直接在内存中操作,没有磁盘 I/O 和复杂的持久化机制,消息传递速度极快。
- 实现简单: API 非常简单,几行代码就能建立起一个消息通道。
- 完美的解耦: 发布者和订阅者不需要知道彼此的存在。
致命缺陷与生产环境陷阱:
- 消息无持久化
- 问题: Redis 不会对发布的消息进行任何形式的持久化。如果消息被发布时,没有任何订阅者在线,那么这条消息将永远丢失。
- 场景: 订阅者客户端因为网络抖动、重启或崩溃而断开连接,在它重连期间的所有消息都无法收到。
- 无消息积压能力
- 问题: Redis 不会为频道维护一个消息队列。它只负责将消息实时地分发给当前在线的订阅者。它不具备任何消息回溯或积压能力。
- 场景: 如果生产者速度远大于消费者的处理速度,慢的消费者会直接丢失消息,因为消息不会被缓存起来。
- 消费者负载均衡问题
- 问题: 在
SUBSCRIBE模式下,一条消息会被发送给所有订阅了该频道的消费者。这被称为 "广播" 模式。你无法让多个消费者共同消费一个频道的消息来实现负载均衡。 - 场景: 如果你有 3 个消费者进程来处理
image_upload频道的消息,希望它们轮流处理以减轻压力,原生的发布订阅无法做到。一条上传消息会被 3 个进程同时收到,导致重复处理。
- 问题: 在
四、 生产环境选型与实践建议
正因为上述缺陷,Redis 的原生发布订阅绝不应该用于传输重要的、不可丢失的业务消息。
1. 适用场景
- 实时状态广播: 如在线聊天室、游戏内公告、服务器状态通知。这些消息本身是瞬态的,丢失一两条无关紧要。
- 轻量级的配置更新: 通知所有服务实例,某个配置项已更新,需要重新拉取。即使错过通知,服务在下次拉取配置时也能纠正。
- 应用内的事件总线: 在微服务内部,作为不同模块之间的事件传递机制。
2. 不适用场景
- 订单处理、支付通知等关键业务消息。
- 需要保证消息必达的通信。
- 需要消峰填谷的异步任务队列。
3. 生产级替代方案
当你的场景需要可靠性时,请考虑以下方案:
- Redis Streams (Redis 5.0+): 这是 Redis 官方推出的可靠的、支持持久化和积压的消息队列数据结构。它支持消费者组,可以实现真正的负载均衡和至少一次消费语义。这是取代发布订阅用于重要业务的首选 Redis 方案。
- 专业的消息中间件: 如 Apache Kafka (高吞吐、持久化)、RabbitMQ (功能丰富、协议完善)、Apache Pulsar 等。它们提供了更强大的持久化、事务、消息确认和路由机制。
总结
面试官,对于 Redis 发布订阅,我的核心结论是:
Redis 发布订阅是一个优秀的"消息通知"机制,但它是一个糟糕的"消息队列"。
它通过内存中的 pubsub_channels 和 pubsub_patterns 结构,以极高的效率实现了消息的实时广播。然而,其无持久化、无积压、纯广播的特性,决定了它只能用于可容忍消息丢失的非关键、实时性场景。
在技术选型时,如果业务要求消息的可靠性、持久化或消费者负载均衡,我会毫不犹豫地选择 Redis Streams 或专业的消息中间件,而不是原生的发布订阅。
