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 工作流程

  1. 订阅者连接:客户端通过 SUBSCRIBE 命令连接到 Redis 服务器,并指定要监听的频道。
  2. 频道注册:Redis 服务器维护一个“频道-订阅者”映射表,将订阅者加入对应频道的订阅者列表。
  3. 消息发布:发布者通过 PUBLISH 命令向指定频道发送消息。
  4. 消息分发:Redis 服务器遍历该频道的所有订阅者,将消息逐条推送给每个订阅者的连接缓冲区。
  5. 实时接收:订阅者从连接中读取消息,完成一次消息传递。
关键特性:Pub/Sub 是“即发即忘”(Fire-and-Forget)模式。消息不会在 Redis 服务器中持久化,一旦发送,服务器不会保留消息副本。如果订阅者不在线,消息将永久丢失。
Pub/Sub 消息广播流程 Publisher Channel: news Subscriber A Subscriber B Subscriber C PUBLISH news “Hello” 消息被广播给所有订阅者,但服务器不存储消息。订阅者离线则消息丢失。

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 会强制断开该订阅者的连接,导致后续消息丢失。

重要配置:Pub/Sub 订阅者的输出缓冲区限制由 client-output-buffer-limit pubsub 32mb 8mb 60 控制。默认值为 32MB 硬限制、8MB 软限制、持续 60 秒。如果订阅者消费过慢,应调整此配置或优化消费者处理逻辑。

4.3.4 服务器故障

Redis 服务器宕机或重启时,所有未持久化的 Pub/Sub 消息都会丢失。即使启用了 RDB 或 AOF 持久化,Pub/Sub 消息也不会被保存。

Pub/Sub 消息丢失风险场景 Publisher Channel: events 订阅者离线 消息直接丢失 缓冲区溢出 连接被强制断开 服务器故障 所有消息丢失 Pub/Sub 无持久化、无确认、无重试机制。消息丢失是设计特性,而非缺陷。 对可靠性要求高的场景,应使用 List 队列或专业的消息队列(如 RabbitMQ、Kafka)。

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,防止慢消费者导致连接断开。

4.5 总结