一、Stream 是什么?为什么需要它?
Redis Stream 是 Redis 5.0 引入的一种持久化、可追加、支持消费者组的消息队列数据结构 。它解决了传统 LIST(缺乏消息确认)和 PUB/SUB(非持久化、无重试机制)在构建可靠消息系统时的短板。
1. 与 List 和 Pub/Sub 的对比
| 特性 | LIST | PUB/SUB | STREAM |
|---|---|---|---|
| 消息持久化 | ✅(但无元数据) | ❌ | ✅(带 ID、时间戳、字段值) |
| 多消费者支持 | ❌(竞争消费) | ✅(广播) | ✅(通过消费组实现负载均衡) |
| 消息确认(ACK) | ❌ | ❌ | ✅ |
| 消息回溯 | ❌(需自行维护) | ❌ | ✅(按 ID 或时间范围) |
| 阻塞读取 | ✅(BLPOP) | ✅ | ✅(XREAD BLOCK) |
结论 :Stream 是 Redis 中唯一原生支持"可靠消息队列"语义的数据结构。
二、Stream 底层原理
Stream 基于 Radix Tree + Listpack 实现:
- Entry ID :格式为
<毫秒时间戳>-<序列号>(如1710234567890-0),保证全局有序。 - 内部存储 :每个节点是一个 Listpack(紧凑型内存结构),存储多个字段-值对。
- 索引优化 :Radix Tree 快速定位 ID 范围,支持高效范围查询(
XRANGE)。
这种设计在高吞吐写入 与低内存占用之间取得平衡,适合日志、事件等高频写场景。
三、核心命令详解
下表归纳了最常用命令及其复杂度:
| 命令 | 作用 | 时间复杂度 | 典型用途 |
|---|---|---|---|
XADD key id field value [field value ...] |
向 Stream 追加消息 | O(1) | 生产者写入事件 |
XREAD [BLOCK ms] STREAMS key id |
读取消息(支持阻塞) | O(N+M),N=流数,M=返回消息数 | 消费者拉取消息 |
XRANGE key start end [COUNT n] |
按 ID 范围查询 | O(N),N=返回消息数 | 调试、回溯 |
XDEL key id [id ...] |
删除消息(仅标记,不释放内存) | O(1) per ID | 清理敏感数据 |
XGROUP CREATE key groupname id [MKSTREAM] |
创建消费组 | O(1) | 初始化消费者组 |
XREADGROUP GROUP group consumer STREAMS key > |
从消费组读取新消息 | O(N+M) | 消费组消费 |
XACK key group id [id ...] |
确认消息已处理 | O(1) per ID | 避免重复消费 |
XPENDING key group [start end count] [consumer] |
查看挂起消息 | O(N) | 监控未 ACK 消息 |
XCLAIM key group new_consumer min_idle_time id [id ...] [IDLE ms] [TIME unix-time-ms] |
将消费组中处于 Pending Entries List(PEL)中的消息从原消费者转移给新消费者,常用于故障恢复或消息重试 | O(N + M),其中 N 是待认领的消息数量,M 是 PEL 中需更新的元数据开销(通常视为 O(1) 每条消息) | 当某个消费者宕机或处理超时时,由其他消费者主动接管其未 ACK 的消息,实现高可用消费;也可用于手动重试积压消息 |
提示 :
>表示"只读取新消息",0表示"从头开始"。
四、消费组(Consumer Group)机制详解
消费组是 Redis Stream 实现多消费者协作消费的核心。
1. 关键概念
- Group:逻辑分组,每个 Stream 可有多个 Group。
- Consumer:组内具体消费者(由名字标识),自动注册。
- Pending Entries List (PEL):记录已分发但未 ACK(Acknowledgment,确认) 的消息。
- Last Delivered ID:组内最后分发的 ID,用于恢复消费位点。
2. 消息生命周期
- 生产者
XADD写入消息。 - 消费者调用
XREADGROUP获取消息,消息进入 PEL。 - 消费成功 →
XACK,消息从 PEL 移除。 - 消费失败/超时 → 其他消费者可通过
XPENDING+XCLAIM接管消息。
3. 故障恢复
- 若消费者宕机,其 PEL 中的消息可被其他消费者通过
XCLAIM接管。 - 重启后可通过
XREADGROUP从0或>继续消费(取决于业务需求)。
4. Stream + 消费组消息流转

5. 消息确认与重试机制

五、典型应用场景
1. 微服务异步通信
- 场景:订单服务 → 库存服务 → 通知服务
- 优势:解耦、削峰、失败重试
- 架构:每个服务作为独立 Consumer Group,确保消息不丢失
2. 实时日志收集
- 场景:前端埋点 → Stream → 日志分析服务
- 优势:高吞吐写入、按时间回溯、支持多分析任务并行消费
3. 事件溯源(Event Sourcing)
- 场景:用户操作流(注册→登录→支付)作为不可变事件存入 Stream
- 优势:天然有序、可重放、支持状态重建
六、核心命令实操记录
1. 创建 Stream 并写入初始数据
首先,创建一个名为 app_logs 的 Stream,并向其中写入几条日志消息:
bash
# 写入 3 条日志消息
XADD app_logs * level "INFO" service "user" event "login" user_id "1001"
XADD app_logs * level "ERROR" service "order" event "timeout" order_id "5001"
XADD app_logs * level "WARN" service "cache" event "miss" key "profile:1001"
假设返回的 Entry ID 分别为:
1710432000000-01710432000001-01710432000002-0
可以使用 XRANGE app_logs - + 查看所有已写入的消息,以确认数据正确无误。
2. 创建消费组
为了实现多消费者的负载均衡与消息确认机制,我们需要为 app_logs 创建一个消费组:
bash
# 删除旧组(如果存在)
XGROUP DESTROY app_logs alert_group
# 重新创建,从头开始读取所有消息
XGROUP CREATE app_logs alert_group 0
注意:使用
0表示从第一条消息开始消费;若想仅处理新消息,则应使用$。
3. 使用 XREADGROUP 拉取消息
接下来,我们可以用 XREADGROUP 从消费组中拉取消息。这里我们将模拟 consumer-A 消费者的行为:
XREADGROUP ... > 返回结果示例:
bash
XREADGROUP GROUP alert_group consumer-A COUNT 2 STREAMS app_logs >
bash
1) 1) "app_logs" # Stream 名称
2) 1) 1) "1710432000000-0" # 消息 ID 1
2) 1) "level"
2) "INFO"
3) "service"
4) "user"
5) "event"
6) "login"
7) "user_id"
8) "1001"
2) 1) "1710432000001-0" # 消息 ID 2
2) 1) "level"
2) "ERROR"
3) "service"
4) "order"
5) "event"
6) "timeout"
7) "order_id"
8) "5001"
数据结构解析:
XREADGROUP 的返回是一个嵌套数组,包含:
-
Stream 名称 :如
app_logs -
消息列表 :每条消息由一个 ID 和字段-值对组成,例如:
bash1) "1710432000000-0" # 消息 ID 2) 1) "level" 2) "INFO" 3) "service" 4) "user" ...
这意味着每条消息都带有一个唯一的 ID 和若干键值对(字段)。
XREADGROUP ... 0返回结果示例:
bash
XREADGROUP GROUP alert_group consumer-A COUNT 2 STREAMS app_logs 0
bash
1) 1) "app_logs" # Stream 名称
2) 1) 1) "1710432000000-0" # 消息 ID 1
2) 1) "level"
2) "INFO"
3) "service"
4) "user"
5) "event"
6) "login"
7) "user_id"
8) "1001"
2) 1) "1710432000001-0" # 消息 ID 2
2) 1) "level"
2) "ERROR"
3) "service"
4) "order"
5) "event"
6) "timeout"
7) "order_id"
8) "5001"
| 特性 | XREADGROUP ... > |
XREADGROUP ... 0(或具体 ID) |
|---|---|---|
| 消息来源 | Stream 中尚未被该消费组消费过的新消息 | 消费组 PEL(Pending Entries List)中已分发但未 ACK 的消息 |
| 是否进入 PEL | ✅ 是(新消息首次分配,自动加入 PEL) | ❌ 否(消息已在 PEL 中,只是重新读取) |
| 是否支持负载均衡 | ✅ 是(Redis 自动分配给不同消费者) | ❌ 否(只能读取属于指定消费者的 PEL 消息) |
| 典型用途 | 正常消费流程(主路径) | 故障恢复 / 重试(异常路径) |
| 能否读到历史消息? | 取决于 XGROUP CREATE 时的起始 ID: - 若为 0 → 能 - 若为 $ → 不能 |
不能(除非之前已用 > 拉取过并未 ACK) |
| 重复调用结果 | 每次返回新的未消费消息 | 每次返回相同的未 ACK 消息 |
4. 查看 Pending Entries List (PEL)
执行完 XREADGROUP 后,这两条消息已被加入 PEL(Pending Entries List),表示它们正在被处理但尚未确认。
bash
XPENDING app_logs alert_group
返回:
bash
1) (integer) 2 # 共 2 条未 ACK
2) "1710432000000-0" # 最早 ID
3) "1710432000001-0" # 最晚 ID
4) 1) 1) "consumer-A"
2) "2" # consumer-A 有 2 条挂起
再查看具体挂起的消息详情:
bash
XPENDING app_logs alert_group - + 10
返回:
bash
1) 1) "1710432000000-0"
2) "consumer-A"
3) (integer) 125000 # 空闲毫秒数(约 125 秒)
4) (integer) 1 # 已投递 1 次
2) 1) "1710432000001-0"
2) "consumer-A"
3) (integer) 125000
4) (integer) 1
5. 成功处理后调用 XACK
假设第一条消息处理成功,我们可以调用 XACK 来确认这条消息:
bash
XACK app_logs alert_group 1710432000000-0
返回 :(integer) 1(表示 1 条确认成功)
再次检查 PEL:
bash
XPENDING app_logs alert_group
现在只剩一条未确认消息:
bash
1) (integer) 1
2) "1710432000001-0"
3) "1710432000001-0"
4) 1) 1) "consumer-A"
2) "1"
6. 模拟失败 ------ 使用 XCLAIM 接管
假设 consumer-A 宕机,可以让 consumer-B 接管超时未处理的消息:
bash
# 接管空闲超过 100 秒的消息
XCLAIM app_logs alert_group consumer-B 100000 1710432000001-0
返回:
bash
1) 1) "1710432000001-0"
2) 1) "level"
2) "ERROR"
3) "service"
4) "order"
5) "event"
6) "timeout"
7) "order_id"
8) "5001"
此时,consumer-B 应该处理这条消息,并在完成后调用 XACK:
bash
XACK app_logs alert_group 1710432000001-0
最终,PEL 应为空:
bash
XPENDING app_logs alert_group
# 返回: (integer) 0
总结
通过上述步骤,展示了如何使用 XREADGROUP 及其相关命令来实现高效的 Redis Stream 消息消费流程。关键点包括:
- 创建 Stream 和消费组:确保 Stream 存在且配置正确的消费组。
- 使用
XREADGROUP拉取消息:每次拉取时,消息会进入 PEL,等待确认。 - 监控 Pending Entries List :定期运行
XPENDING,及时发现并处理积压消息。 - 故障恢复与重试 :利用
XCLAIM实现消费者宕机后的消息接管,保障系统高可用性。
七、高频面试题
Q1:Stream 的消息 ID 是如何生成的?可以自定义吗?
答 :默认格式为 <毫秒时间戳>-<序列号>(如 1710234567890-0)。可通过 XADD key * ... 自动生成;也可手动指定(但必须大于当前最大 ID,否则报错)。
Q2:消费组中的消息未 ACK 会怎样?
答 :消息会保留在 Pending Entries List (PEL) 中,不会被再次分发给同一组的其他消费者,除非使用 XCLAIM 主动接管。长期未 ACK 可能导致内存堆积。
Q3:如何监控 Stream 的积压情况?
答 :使用 XPENDING key group 查看挂起消息数量和分布;结合 XINFO STREAM key 查看总长度和消费者组信息。
Q4:Stream 支持消息 TTL 吗?
答 :不直接支持。但可通过 XADD ... MAXLEN ~ N 限制长度(近似滑动窗口),或定期用 XTRIM 手动清理旧消息。
Q5:XREAD 和 XREADGROUP 有什么区别?
答 :XREAD 是普通读取,无消费组语义;XREADGROUP 必须指定 Group 和 Consumer,会将消息加入 PEL 并支持 ACK,适用于多消费者协作场景。