主题
第二十章:消息队列选型与 Stream
Q62:Redis 里做消息队列,List、Pub/Sub、Stream 有什么区别?分别适合什么场景?
答:
Redis 里能当「消息」或「队列」用的结构有三个:List、Pub/Sub、Stream。它们的能力差异很大,选错会导致消息丢失或消费不可靠。核心区别在于是否持久化、是否支持确认(ACK)、是否支持多消费者。
一、三种结构对比
| 维度 | List | Pub/Sub | Stream |
|---|---|---|---|
| 消息是否持久化 | 是(存在 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消费组的三个关键规则:
- 组内竞争:同一个消费组内的多个消费者,一条消息只会被其中一个消费(点对点);
- 组间广播:不同消费组各自独立消费,一条消息可以被每个组各消费一次(广播);
- 独立游标:每个消费组维护自己的消费进度(最后投递的消息 ID),互不影响。
二、创建消费组
bash
# 创建消费组 video-workers,从 Stream 开头(0)开始消费
redis-cli XGROUP CREATE stream:task video-workers 0 MKSTREAMMKSTREAM:如果 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-0python
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-0XCLAIM 把「卡在消费者 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 Stream | RabbitMQ | Kafka |
|---|---|---|---|
| 定位 | 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:超高吞吐、流处理、长期保存,适合日志与大数据流;
- 选型核心看吞吐量、可靠性要求、消息特性需求、运维与生态,而非「谁更先进」。