Skip to content

第二十章:消息队列选型与 Stream ​

Q62:Redis 里做消息队列,List、Pub/Sub、Stream 有什么区别?分别适合什么场景? ​

答:

Redis 里能当「消息」或「队列」用的结构有三个:List、Pub/Sub、Stream。它们的能力差异很大,选错会导致消息丢失或消费不可靠。核心区别在于是否持久化、是否支持确认(ACK)、是否支持多消费者。

一、三种结构对比 ​

维度ListPub/SubStream
消息是否持久化是(存在 Redis 内存/持久化中)否(即发即弃,不落盘)是(消息保留在 Stream 中)
消费模型点对点(一条消息被一个消费者取走)广播(所有订阅者都收到)点对点 + 消费组广播两种都支持
是否支持 ACK否(弹出即消费,没有确认机制)否是(XACK 确认,未确认可重投)
是否支持阻塞消费是(BRPOP/BLPOP)是(SUBSCRIBE)是(XREAD BLOCK / XREADGROUP BLOCK)
消费者离线补消息可(消息还在 List 里)否(离线期间消息全部丢失)可(消息保留,可从任意位置重读)
多消费者 / 消费组否(一个元素只能被弹掉一次)支持广播,但不区分消费组支持(XGROUP 消费组,组内竞争、组间广播)
消息可回溯部分(弹出后无法重读)否是(按 ID 范围重读历史消息)
典型场景简单任务队列、最新列表实时广播、配置变更通知可靠消息队列、事件溯源、需要 ACK 和重试的异步任务
实现复杂度低低中(要理解消息 ID、消费组、pending)

二、List 适合简单队列 ​

List 用 LPUSH(入队)+ BRPOP(阻塞弹出)实现 FIFO 队列,简单高效,是之前文档反复讲过的模型。

python
redis_client.lpush("task:queue", task)          # 生产者入队
task = redis_client.brpop("task:queue", 5)      # 消费者阻塞取出

局限:没有 ACK,消息 BRPOP 弹出后就从队列消失了。消费者取到消息后、处理前崩溃,这条消息就丢了;也没有「多个消费者组各消费一份」的能力。

三、Pub/Sub 适合实时广播 ​

Pub/Sub 是「发布-订阅」模型,一条消息会广播给所有订阅了该频道的订阅者,且不持久化。

python
# 订阅端
pubsub = redis_client.pubsub()
pubsub.subscribe("config:changed")

# 发布端
redis_client.publish("config:changed", "reload")

致命点:订阅者离线期间的消息直接丢失,无法补。所以 Pub/Sub 只能用于「实时通知、配置广播」这类丢失可接受的场景,不能当可靠队列。

四、Stream 是「数据结构 + 消息队列」的结合 ​

Stream(Redis 5.0+)是专门为消息队列设计的数据结构。它像「追加日志」,每条消息有唯一递增的 ID,消息写进去后会保留在 Stream 里,直到你主动删除。它补齐了 List 和 Pub/Sub 的短板:支持 ACK、消费组、消息回溯。

text
Stream 结构示意
stream:task
  1690000000000-0  ──► { task_id: 6, stage: SCRIPT }
  1690000001000-0  ──► { task_id: 6, stage: IMAGE }
  1690000002000-0  ──► { task_id: 7, stage: SCRIPT }
  (消息按 ID 追加,读取后仍保留,直到 XDEL / XTRIM)

五、选型建议 ​

需求推荐理由
简单任务队列、能容忍消费端崩溃丢任务List简单、快速
实时通知、配置广播、可容忍丢消息Pub/Sub即发即弃、低延迟
可靠消息、需要 ACK、重试、消费组、消息回溯Stream持久化 + 确认 + 消费组
需要完整 MQ 能力(死信、延迟、跨语言生态、多协议)专业 MQ(Kafka/RabbitMQ/RocketMQ)Redis Stream 能力有限,专业 MQ 更完善

一句话总结:List 够用就 List,要广播就 Pub/Sub,要可靠就 Stream,要工业级完整能力就上专业 MQ。


Q63:Redis Stream 的消费组模型是怎样的?如何用它实现「多个消费者不重复消费 + 故障恢复」? ​

答:

Stream 最核心的价值之一就是消费组(Consumer Group)。它解决了传统 List 队列的两个痛点:多消费者竞争时「谁取到算谁」的不可靠性,以及消费者崩溃后的「消息丢失」。

一、消费组模型 ​

text
stream:task(消息流,按 ID 追加)
   │
   ├── 消费组 group-1
   │      ├─ consumer-A
   │      └─ consumer-B
   │
   └── 消费组 group-2
          └─ consumer-C

消费组的三个关键规则:

  1. 组内竞争:同一个消费组内的多个消费者,一条消息只会被其中一个消费(点对点);
  2. 组间广播:不同消费组各自独立消费,一条消息可以被每个组各消费一次(广播);
  3. 独立游标:每个消费组维护自己的消费进度(最后投递的消息 ID),互不影响。

二、创建消费组 ​

bash
# 创建消费组 video-workers,从 Stream 开头(0)开始消费
redis-cli XGROUP CREATE stream:task video-workers 0 MKSTREAM
  • MKSTREAM:如果 Stream 不存在则自动创建;
  • 0:消费组从消息 ID 为 0 的位置开始(即从头读历史消息)。如果只想消费「新消息」,用 $ 表示从当前最新位置开始。

三、消费消息:XREADGROUP ​

bash
# 组内消费者 worker-1 读取最多 10 条新消息,阻塞 5 秒等待
redis-cli XREADGROUP GROUP video-workers worker-1 COUNT 10 BLOCK 5000 STREAMS stream:task >

> 是关键符号:表示「只读取我这个消费者还没见过的新消息」。这也是消费组与普通 XREAD 的区别——XREADGROUP ... > 会把读到的消息分配到该消费者名下,并记录到 pending(待确认)列表。

python
messages = redis_client.xreadgroup(
    "video-workers", "worker-1",
    {"stream:task": ">"},
    count=10, block=5000,
)

四、确认消息:XACK ​

消费者处理完消息后,必须 XACK 确认,消息才会从该消费者的 pending 列表移除,Redis 才会认为「这条消息被这个消费者成功消费」。

bash
redis-cli XACK stream:task video-workers 1690000000000-0
python
redis_client.xack("stream:task", "video-workers", message_id)

五、故障恢复:pending 列表(Pending Entries List) ​

如果一个消费者取出消息后、XACK 之前崩溃了,这条消息会一直停在它的 pending 列表里(未确认状态)。这就是「不丢失」的关键:

text
消费者 A 取出消息 msg-1
   └─► msg-1 进入 A 的 pending 列表(未确认)
         │
         ├─ 正常:处理完 → XACK msg-1 → 移出 pending
         └─ 崩溃:msg-1 留在 pending,永不确认

恢复机制:其他消费者(或原消费者重启后)可以接管 pending 列表里未确认的消息。

bash
# 查看某个消费者的 pending 消息
redis-cli XPENDING stream:task video-workers

# 把某个消费者 pending 里超过 60 秒未确认的消息,转交给另一个消费者重处理
redis-cli XCLAIM stream:task video-workers worker-2 60000 1690000000000-0

XCLAIM 把「卡在消费者 A pending 里超过 N 毫秒」的消息,转移到消费者 B 名下重新投递。这是 Stream 实现「故障转移 + 消息重试」的核心命令。

六、完整可靠性闭环 ​

text
生产:XADD stream:task * field value       (消息从没丢,一直保留)
消费:XREADGROUP GROUP g consumer >        (投递给组内某消费者,进入 pending)
处理:业务处理成功
确认:XACK stream:task g message_id         (从 pending 移除)
异常:处理失败 / 消费者崩溃
恢复:XPENDING 查未确认,XCLAIM 转交重投        (消息不丢,可重试)

七、小结 ​

  • 消费组 = 组内竞争(不重复)+ 组间广播(各消费一份)+ 独立游标;
  • XREADGROUP ... > 消费新消息,XACK 确认;
  • pending 列表 + XPENDING/XCLAIM 是实现「消费者崩溃不丢消息、可故障恢复」的关键;
  • Stream 适合需要「可靠 + ACK + 重试」的异步任务队列。

Q64:Redis Stream 和 RabbitMQ / Kafka 相比,什么时候该用哪个? ​

答:

Redis Stream 提供了「类 MQ」的能力,但它和专业消息中间件(RabbitMQ、Kafka、RocketMQ)定位不同。选择的关键在于:你要的是「一个足够用的轻量队列」,还是「一个企业级的消息平台」。

一、能力对比 ​

维度Redis StreamRabbitMQKafka
定位Redis 内置的可靠队列数据结构通用消息中间件分布式流处理 / 高吞吐日志
持久化依赖 Redis 的 RDB/AOF是,落盘是,落盘 + 副本
消息确认 ACK支持(XACK + pending)支持支持(offset 提交)
消费模型消费组(组内竞争 + 组间广播)队列 + 交换机(多种路由)分区 + 消费组
延迟消息需自己用 ZSet 实现有插件 / 死信队列可配合无原生延迟,需自行实现
死信队列需自己实现原生支持需自行实现
消息回溯支持(按 ID 范围读)一般不支持(消费即删)天然支持(按 offset)
吞吐量中(取决于 Redis 性能)中高(顺序写 + 分区并行)
顺序保证单 Stream 内按 ID 顺序队列内顺序分区内顺序,跨分区无序
运维成本低(复用现有 Redis)中高
生态 / 多语言Redis 客户端覆盖广完整、协议标准完整、大数据生态强
是否独立组件否(Redis 的一部分)是是

二、选 Redis Stream 的场景 ​

  • 项目里已经在用 Redis,不想再引入一套中间件,只想「要一个可靠的队列」;
  • 消息量适中,Redis 的性能足够扛;
  • 需要消息回溯 / 事件溯源(Stream 天生支持按 ID 重读);
  • 团队对 Kafka/RabbitMQ 运维不熟,或没有专职中间件团队;
  • 需要轻量、部署简单、快速落地。

典型例子:视频任务队列、异步通知、事件日志。

三、选 RabbitMQ 的场景 ​

  • 需要复杂的路由(交换机 + 绑定,按规则分发到不同队列);
  • 需要原生死信队列、延迟队列、优先级队列等成熟特性;
  • 消息可靠性要求极高、业务语义复杂(如金融、订单);
  • 需要标准化协议(AMQP)和多语言互操作。

四、选 Kafka 的场景 ​

  • 超高吞吐、海量日志/埋点/流式数据(每秒百万级消息);
  • 需要长期保存消息、反复消费、按分区并行处理;
  • 大数据生态(配合 Flink、Spark 等做实时计算);
  • 多个下游系统订阅同一份数据流;

五、一个常见误区 ​

很多人以为「Stream 就是 Redis 版的 Kafka」,其实不全对。Stream 补上了 Redis 做队列时最缺的「ACK + 消费组」,但它在吞吐、副本一致性、多租户、运维工具、生态上,和专业 MQ 仍有差距。可以把它理解成:

text
Redis Stream ≈ 「可靠的、支持 ACK 和消费组的轻量队列」,介于 List 和专业 MQ 之间

它不是 Kafka/RabbitMQ 的替代品,而是在「够用」的场景里,帮你省掉一个组件。

六、选型决策建议 ​

text
已有 Redis、要可靠队列、量适中 ──► Redis Stream
要复杂路由 / 死信 / 延迟 / 强可靠 ──► RabbitMQ
要超高吞吐 / 流处理 / 大数据生态 ──► Kafka
只是简单排队、能容忍丢失 ──► List / 继续用 BRPOP

七、小结 ​

  • Redis Stream:轻量、复用 Redis、支持 ACK/回溯,适合「够用就好」的可靠队列;
  • RabbitMQ:路由灵活、特性丰富、强可靠,适合业务语义复杂的消息场景;
  • Kafka:超高吞吐、流处理、长期保存,适合日志与大数据流;
  • 选型核心看吞吐量、可靠性要求、消息特性需求、运维与生态,而非「谁更先进」。