[Redis小技巧10]深入 Redis Stream:从原理到生产级实践

一、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. 消息生命周期

  1. 生产者 XADD 写入消息。
  2. 消费者调用 XREADGROUP 获取消息,消息进入 PEL。
  3. 消费成功 → XACK,消息从 PEL 移除。
  4. 消费失败/超时 → 其他消费者可通过 XPENDING + XCLAIM 接管消息。

3. 故障恢复

  • 若消费者宕机,其 PEL 中的消息可被其他消费者通过 XCLAIM 接管。
  • 重启后可通过 XREADGROUP0> 继续消费(取决于业务需求)。

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-0
  • 1710432000001-0
  • 1710432000002-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 和字段-值对组成,例如:

    bash 复制代码
    1) "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,适用于多消费者协作场景。

相关推荐
扑克中的黑桃A2 小时前
基于代价模型的连接条件下推:复杂SQL查询的性能优化实践
数据库
数据知道2 小时前
MongoDB分片集群监控:详解Balancer状态与Chunk分布分析
数据库·mongodb
⑩-2 小时前
Redis内存淘汰策略?如何处理大Key?
java·数据库·redis
Y001112363 小时前
Day3-MySQL-SQL-2
数据库·sql·mysql
V1ncent Chen3 小时前
从零学SQL 07 数据过滤
数据库·sql·mysql·数据分析
A10169330713 小时前
maven导入spring框架
数据库·spring·maven
代码探秘者3 小时前
【Java集合】ArrayList :底层原理、数组互转与扩容计算
java·开发语言·jvm·数据库·后端·python·算法
末点3 小时前
超长文本格式坐标串数据空间化入库
数据库·c#·st_geomfromtext
qqacj3 小时前
如何使用Spring Boot框架整合Redis:超详细案例教程
spring boot·redis·后端