4. 基于 Pub/Sub 的发布订阅模式:SUBSCRIBE/PUBLISH 实现广播消息、与 List 队列的区别、消息丢失风险
Redis 的 Pub/Sub(发布/订阅)是一种消息通信模式,发送者(Publisher)将消息发送到指定的频道(Channel),而订阅者(Subscriber)可以订阅一个或多个频道并实时接收消息。这种模式在广播通知、实时数据推送、事件驱动架构中应用广泛。本章将深入剖析 Pub/Sub 的核心机制、与 List 队列的差异以及其固有的消息丢失风险。
4.1 Pub/Sub 核心机制与命令
Pub/Sub 模式的核心是“频道”概念。发布者向频道发送消息,所有订阅该频道的客户端都会立即收到消息。这种模式是典型的“扇出”(Fan-out)模型,消息被广播给所有订阅者。
4.1.1 基本命令
| 命令 | 功能 | 时间复杂度 |
|---|---|---|
PUBLISH channel message |
向指定频道发送消息 | O(N),N 为订阅该频道的客户端数量 |
SUBSCRIBE channel [channel ...] |
订阅一个或多个频道 | O(N),N 为订阅的频道数 |
UNSUBSCRIBE [channel [channel ...]] |
退订一个或多个频道 | O(N),N 为退订的频道数 |
PSUBSCRIBE pattern [pattern ...] |
订阅一个或多个符合 glob 风格模式的频道 | O(N),N 为订阅的模式数 |
PUBSUB CHANNELS [pattern] |
列出当前活跃的频道(至少有一个订阅者) | O(N),N 为活跃频道数 |
PUBSUB NUMSUB [channel ...] |
返回指定频道的订阅者数量 | O(N),N 为查询的频道数 |
4.1.2 工作流程
- 订阅者连接:客户端通过
SUBSCRIBE命令连接到 Redis 服务器,并指定要监听的频道。 - 频道注册:Redis 服务器维护一个“频道-订阅者”映射表,将订阅者加入对应频道的订阅者列表。
- 消息发布:发布者通过
PUBLISH命令向指定频道发送消息。 - 消息分发:Redis 服务器遍历该频道的所有订阅者,将消息逐条推送给每个订阅者的连接缓冲区。
- 实时接收:订阅者从连接中读取消息,完成一次消息传递。
4.2 Pub/Sub 与 List 队列的对比
虽然 Pub/Sub 和 List 队列都能实现消息传递,但它们在设计哲学、消息模型、可靠性等方面存在本质差异。理解这些差异对于选择正确的消息模式至关重要。
| 对比维度 | Pub/Sub(发布/订阅) | List(列表队列) |
|---|---|---|
| 消息模型 | 广播(Fan-out):一条消息发送给所有订阅者 | 点对点(Point-to-Point):一条消息通常被一个消费者消费 |
| 消息存储 | 不存储,实时推送 | 存储在 List 数据结构中,持久化 |
| 消息可靠性 | 低:订阅者离线则消息丢失 | 高:消息持久化,消费者可随时拉取 |
| 消费方式 | 推送(Push):服务器主动推送给订阅者 | 拉取(Pull):消费者主动从 List 中弹出消息 |
| 消息确认 | 无确认机制 | 可通过 RPOPLPUSH 或 BRPOPLPUSH 实现可靠确认 |
| 消费者关系 | 一对多:一个频道对应多个订阅者 | 多对一:多个消费者竞争消费同一个 List |
| 典型场景 | 实时通知、广播、聊天室、事件分发 | 任务队列、延迟任务、异步处理 |
| 性能瓶颈 | 订阅者数量多时,推送压力集中在服务器 | 消费者数量多时,竞争锁和轮询开销增加 |
- 如果业务要求每条消息必须被可靠处理(如订单处理、支付回调),请使用 List 队列 +
BRPOPLPUSH实现可靠消费。 - 如果业务需要实时广播,且允许少量消息丢失(如实时行情推送、系统状态通知),Pub/Sub 是更轻量、更低延迟的选择。
4.3 消息丢失风险分析
Pub/Sub 模式最大的缺陷在于消息丢失风险。以下场景会导致消息无法到达订阅者:
4.3.1 订阅者离线
如果订阅者在消息发布时未连接到 Redis 服务器,该消息将永久丢失。Pub/Sub 没有消息持久化或重传机制。
4.3.2 网络中断
当订阅者与 Redis 之间的网络连接中断时,中断期间发布的消息会丢失。Redis 不会为断线的订阅者缓存消息。
4.3.3 客户端缓冲区溢出
如果订阅者消费消息的速度慢于发布者发送消息的速度,客户端的输出缓冲区可能被填满。当缓冲区达到 client-output-buffer-limit 配置的限制时,Redis 会强制断开该订阅者的连接,导致后续消息丢失。
client-output-buffer-limit pubsub 32mb 8mb 60 控制。默认值为 32MB 硬限制、8MB 软限制、持续 60 秒。如果订阅者消费过慢,应调整此配置或优化消费者处理逻辑。
4.3.4 服务器故障
Redis 服务器宕机或重启时,所有未持久化的 Pub/Sub 消息都会丢失。即使启用了 RDB 或 AOF 持久化,Pub/Sub 消息也不会被保存。
4.4 实战:使用 Pub/Sub 实现广播通知
以下示例展示如何使用 Redis Pub/Sub 实现一个简单的系统状态广播通知。发布者定期发送系统健康状态,所有订阅的管理员客户端实时接收。
4.4.1 发布者代码(Python)
import redis
import time
import json
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
channel = 'system:health'
while True:
status = {
'cpu': '45%',
'memory': '62%',
'disk': '78%',
'timestamp': time.time()
}
message = json.dumps(status)
# 发布消息到频道
r.publish(channel, message)
print(f"Published: {message}")
time.sleep(5) # 每5秒发布一次
4.4.2 订阅者代码(Python)
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
channel = 'system:health'
pubsub = r.pubsub()
# 订阅频道
pubsub.subscribe(channel)
print(f"Subscribed to {channel}, waiting for messages...")
for message in pubsub.listen():
if message['type'] == 'message':
print(f"Received: {message['data']}")
# 在这里处理消息,例如发送告警或更新仪表盘
- 订阅者必须保持长连接,不能频繁断开重连,否则会丢失中间消息。
- 如果订阅者需要处理多种类型的消息,可以使用
PSUBSCRIBE订阅模式频道,例如system:*匹配所有以system:开头的频道。 - 在生产环境中,建议为 Pub/Sub 订阅者设置合理的
client-output-buffer-limit,防止慢消费者导致连接断开。