谈谈 Redis 5.0 中的 Stream 消息队列?
结论:Stream 是 Redis 5.0 引入的追加日志型数据结构,补齐了 Pub/Sub 与 List 缺失的"消费组 + ack + 积压查看"三件套。它适合"轻量、可容忍单机容量、需要至少一次投递"的场景(如站内事件、异步任务),不适合替代 Kafka 那种高吞吐、多副本、长期堆积的消息系统。
一、核心概念对照
| 概念 | Stream | 类比 |
|---|---|---|
| 消息 ID | 毫秒时间戳-序号,单调递增 | Kafka 的 offset |
| 消费组 | XGROUP CREATE 创建,组内共享游标 | Kafka consumer group |
| 消费者 | 组内的一个连接标识 | consumer 实例 |
| PEL | pending entries list,已投递未 ack | 无直接对应,Kafka 靠 offset 提交 |
| 读取 | XREAD(不消费)/ XREADGROUP(消费) | poll |
| 确认 | XACK | commit offset |
| 裁剪 | XTRIM MAXLEN ~ 10000 | retention |
二、消费组的工作机制
stream: orders
├─ 1000-0 ┐
├─ 1001-0 ├──► group "g1" 的 last-delivered-id
├─ 1002-0 ┘ │
│ ├─ consumer A 的 PEL:[1000-0, 1001-0]
│ └─ consumer B 的 PEL:[1002-0]
│
├─ XREADGROUP GROUP g1 A COUNT 10 BLOCK 5000 STREAMS orders >
│ ">" 表示只读新消息,读到的消息进入 A 的 PEL
│
├─ XACK orders g1 1000-0 → 从 PEL 移除,表示处理成功
│
└─ XPENDING orders g1 → 看积压:条数、最小/最大 ID、各消费者分布
消费者崩溃后:XAUTOCLAIM 把超时未 ack 的消息转给存活消费者
关键点:消息不会因为被读取而删除,只有 XACK 才从 PEL 移除,XTRIM 才真正裁剪日志。这使"至少一次投递"成为可能,也要求消费端必须幂等。
三、常用命令与代码
# 生产
XADD orders '*' orderId 1001 amount 99
XADD orders MAXLEN ~ 100000 '*' orderId 1002 amount 20
# 建组:$ 表示从尾部开始,0 表示从头消费全部历史
XGROUP CREATE orders g1 0 MKSTREAM
# 消费(阻塞 5 秒,一次 10 条)
XREADGROUP GROUP g1 consumer-A COUNT 10 BLOCK 5000 STREAMS orders '>'
# 确认 + 查看积压 + 认领超时消息
XACK orders g1 1730000000000-0
XPENDING orders g1 - + 10
XAUTOCLAIM orders g1 consumer-B 60000 0-0 COUNT 10
// Spring Data Redis:手动 ack,ack 前失败会留在 PEL 中可重投
streamOps.createGroup("orders", "g1");
List<MapRecord<String, Object, Object>> records =
streamOps.read(Consumer.from("g1", "consumer-A"),
StreamReadOptions.empty().count(10).block(Duration.ofSeconds(5)),
StreamOffset.create("orders", ReadOffset.lastConsumed()));
for (MapRecord<String, Object, Object> r : records) {
handle(r.getValue()); // 业务幂等
streamOps.acknowledge("orders", "g1", r.getId());
}
四、与 List、Kafka 的取舍
- 对比 List:List 的
BLPOP取出即消失,消费者崩溃消息就丢了;Stream 有 PEL,可重投、可查积压。 - 对比 Kafka:Stream 没有分区、没有多副本(靠主从)、容量受内存限制,堆积几十 GB 就会打爆内存;Kafka 靠磁盘顺序写与分区副本可长期堆积。
- 适用判断:消息量在每天百万级以内、可容忍 Redis 单点容量、希望少引入一个中间件时选 Stream;否则选 RocketMQ/Kafka。
五、常见追问
- 问:消费组没 ack 的消息会一直保留吗?答:会留在 PEL 中,直到被 ack、被
XAUTOCLAIM/XCLAIM转走,或整个 stream 被删除;需要监控XPENDING的条数与最老消息年龄。 - 问:
XREADGROUP的>和指定 ID 有什么区别?答:>读从未投递的新消息;指定 ID 则重读该消费者 PEL 中已投递未 ack 的消息,常用于故障恢复后重放。 - 问:Stream 会无限增长吗?答:不会自动裁剪,必须显式
XTRIM或XADD MAXLEN;否则内存持续上涨,最终触发淘汰把别的 key 挤掉。 - 问:多个消费者能重复消费同一条消息吗?答:同一消费组内不会(PEL 归属某个消费者);不同消费组各自维护游标,天然支持"一份数据多方处理"。
- 问:集群模式下 Stream 有什么限制?答:整个 stream 落在单个槽、单个节点,无法像 Kafka 分区那样水平扩展;热点 stream 会打满单节点。
- 问:如何做到"恰好一次"?答:Redis 层只能做到至少一次,恰好一次要靠消费端幂等(唯一键去重表)实现。
小结
- Stream = 追加日志 + 消费组 + PEL + ack,是 Redis 里唯一具备"可靠消费"语义的结构。
- 必须配
MAXLEN裁剪与XPENDING监控,否则内存与积压都会失控。 - 定位是轻量 MQ;容量、堆积、分区扩展这三条一旦成为硬需求,就该换专业消息中间件。