Redis Stream:轻量级的持久化消息队列
一句话结论(30s)
Redis Stream 的本质是填补 List(取走即丢)与 Pub/Sub(不在线即丢)之间的空白——一个带持久化、ACK、消费者组的轻量消息队列:因为消费者组负载均衡 + XACK 确认让消息不丢且可回溯;但消息存内存、无分区,不适合高吞吐大批量,定位是 QPS<1000 场景下零运维成本的替代方案。
核心原理(2min)
XADD 追加消息(* 自动生成 ID),XREAD/XREADGROUP 消费,XACK 显式确认;消费者组内多个消费者负载均衡(同一条消息只发给组内一个消费者,语义同 Kafka),未 ACK 的消息留在 PEL 中可用 XPENDING 排查卡住的消费者。XREADGROUP 支持从上次未 ACK 消息重读(0)或只读新消息(>)。消息存内存、受 RDB/AOF 持久化,所以受内存上限限制、消息积压会爆内存。
底层深入(5-10min)
Redis 做消息队列的三代方案
| 方案 | 持久化 | ACK | 消费者组 | 消息回溯 |
|---|---|---|---|---|
| List (RPUSH/LPOP) | ✅ | ❌ | ❌ | ❌ |
| Pub/Sub | ❌ | ❌ | ❌ | ❌ |
| Stream | ✅ | ✅ | ✅ | ✅ |
List 的问题是”取走就没了”——消费者崩溃消息丢失。Pub/Sub 的问题是”不在线就丢了”——没有持久化。
思考:为什么说 Stream 恰好「填补空白」?把两个前代的缺陷并排放——List 有持久化但消费者拿走后消息就没了、崩溃即丢,Pub/Sub 不持久化且订阅者不在线就丢;Stream 把「持久化 + 显式 ACK + 消费者组」拼在一起:消息取走不删、等 XACK 确认,消费者崩溃后还能从 PEL 重读。想清楚这个「取走不删」的语义转变,就抓住了 Stream 相对 List/Pub/Sub 的质变点。
Stream 的核心命令
XADD mystream * key1 val1 key2 val2 → 添加消息 (* = 自动生成ID)
XREAD COUNT 2 STREAMS mystream 0 → 读取消息(0 = 从头开始)
XREAD BLOCK 0 STREAMS mystream $ → 阻塞等待新消息($ = 只读最新)
XGROUP CREATE mystream mygroup $ → 创建消费者组
XREADGROUP GROUP mygroup consumer1 → 消费者组消费
XACK mystream mygroup msgId → 确认消费
XPENDING mystream mygroup → 查看pending未确认消息
消费者组内多个消费者负载均衡消费——同一条消息只发给组内一个消费者。与 Kafka 的 Consumer Group 语义一致。
消息持久化与 ACK
Stream 消息存储在 Redis 内存中(也受 RDB/AOF 持久化),与 Redis 的键空间一样受内存上限限制。消费者取走消息后不自动删除——必须显式 XACK 确认。未 ACK 的消息保留在 PEL(Pending Entry List)中,可以被 XPENDING 查询到,方便排查”哪个消费者卡住了”。
XREADGROUP 支持从上次未 ACK 的消息重新消费(0)或从新消息开始(>)。
思考:为什么消息取走后不自动删除、还专门维护一个 PEL?反着想——如果取走即删(像 List),消费者处理到一半崩溃,消息就永久丢了;保留消息并记在 PEL 里,就能用 XPENDING 查出「谁卡住了」、用 XACK 确认「处理完了」。这是「至少一次」投递语义的根基:宁可重复消费,也不能丢。代价是消息要等 XACK 才真正释放,积压会占内存。
适用场景
Stream 适合轻量级消息场景——不另起 RocketMQ/Kafka 集群,利用已有的 Redis 实例处理消息。比如异步处理小程序订单状态变更通知,QPS < 1000,部署前已有 Redis,用 Stream 零运维成本。
不适合高吞吐大批量消息——Redis 内存有限(消息积压会爆内存),Stream 没有分区/分片,单 Stream 是单机瓶颈。
思考:为什么 Stream 扛不住高吞吐?两条约束一起看——消息存在内存里,积压直接爆内存;Stream 没有分区,单 Stream 是单机瓶颈、无法像 Kafka 那样水平扩容。所以它的定位是「轻量、够用、零运维」,而不是「替代 Kafka/RocketMQ」。判断选型时先问:QPS 上千吗、消息量会积压吗、需要严格顺序/持久化保证吗——任一命中就该换专业 MQ。
总结
Redis Stream 在”需要持久化 + ACK”的消息场景下,填补了 List 和 Pub/Sub 的空白。没有达到 RocketMQ/Kafka 的成熟度,但在轻量级场景下是不需要额外组件的理想选择。
章末提问
Q1:Redis Stream 相比 List 和 Pub/Sub 解决了什么问题?
回答思路:结论——Stream 用「持久化 + 显式 ACK + 消费者组」补上了 List「取走即丢」和 Pub/Sub「不在线即丢」的空白。因为:List 的 LPOP 取走就删、消费者崩溃消息丢失;Pub/Sub 不持久化、订阅者不在线就漏消息;Stream 消息取走不删、等 XACK 确认,崩溃后还能从 PEL 重读,实现「至少一次」投递。
Q2:Stream 消费者组是怎么实现负载均衡的?消息为什么需要 XACK?
回答思路:结论——同一条消息只会发给组内一个消费者,每个消费者维护自己的 PEL,处理完用 XACK 确认。因为:消费者组内按消息 ID 分配、避免重复消费,类似 Kafka 的 Consumer Group;消息取走不删、留在 PEL,只有 XACK 后才真正释放,未 ACK 的可用 XPENDING 排查「哪个消费者卡住」,这是保证不丢消息的关键。
Q3:Redis Stream 和 Kafka/RocketMQ 相比有什么局限?什么场景才用 Stream?
回答思路:结论——Stream 消息存内存、无分区,扛不住高吞吐大批量,只适合 QPS<1000 的轻量场景。因为:内存有限导致积压会爆内存,单 Stream 是单机瓶颈、无法水平扩容,也没有 Kafka 的分区顺序保证;它的价值是「已有 Redis 时零运维、零额外组件」,比如小程序订单状态通知这类轻量异步任务。