Redis Stream 核心原理与实战指南

Redis Stream 核心原理与实战指南

文章目录

  • [Redis Stream 核心原理与实战指南](#Redis Stream 核心原理与实战指南)
    • [1. Redis Stream 是什么?](#1. Redis Stream 是什么?)
      • [1.1 核心架构概览](#1.1 核心架构概览)
    • [2. 操作逻辑与核心指令](#2. 操作逻辑与核心指令)
      • [2.1 生产消息:XADD](#2.1 生产消息:XADD)
      • [2.2 消费消息:XREADGROUP (核心逻辑)](#2.2 消费消息:XREADGROUP (核心逻辑))
      • [2.3 消息确认:XACK](#2.3 消息确认:XACK)
      • [2.4 状态监控:XINFO (排查神器)](#2.4 状态监控:XINFO (排查神器))
    • [3. Spring Boot 集成实战](#3. Spring Boot 集成实战)
      • [3.1 整体集成架构图](#3.1 整体集成架构图)
      • [3.2 依赖引入](#3.2 依赖引入)
      • [3.3 生产者代码 (使用 RedisTemplate)](#3.3 生产者代码 (使用 RedisTemplate))
      • [3.4 消费者代码 (使用 StreamMessageListenerContainer)](#3.4 消费者代码 (使用 StreamMessageListenerContainer))
      • [3.5 集成中的关键点 (避坑指南)](#3.5 集成中的关键点 (避坑指南))
    • [4. 总结与排查清单](#4. 总结与排查清单)

1. Redis Stream 是什么?

一句话总结:

Redis Stream 是一个 仅追加的日志结构 数据类型,专门用于实现轻量级的消息队列事件溯源。你可以把它想象成一个永久的、链表结构的文件,新的消息只能写在末尾。

1.1 核心架构概览

在代码操作之前,先通过这张图看清楚 Redis Stream 涉及到的所有角色及其关系:
contains
has
contains
manages
tracks (unacked)
1 1 1 1 n n n n 1 n Stream
+List<Entry> entries
+RadixTree index
+String Key(如: file_cmd_message_key)
+createGroup(name)
+addEntry(fields)
Entry
+String ID(时间戳-序号)
+Map<String, String> Fields(键值对)
ConsumerGroup
+String LastDeliveredID
+String Name(如: FILE_CMD_MESSAGE_GROUP)
+PEL(Pending Entries List)
Consumer
+String Name(如: no00, no01)
PEL
+List<PendingEntry> messages
+add(entry, consumer)
+remove(entryID) : // XACK

核心概念解析:

  1. Stream (流) : 一个 Key,对应一个消息队列。比如 file_cmd_message_key
  2. Entry (消息) : 包含一个唯一的 ID 和一组键值对。ID 通常由 Redis 自动生成(格式:timestamp-sequence,如 1700000000000-0)。
  3. Consumer Group (消费组) : 用于实现发布/订阅 模式下的负载均衡。一个 Stream 可以有多个组,每个组独立消费消息(互不干扰)。
  4. Consumer (消费者) : 组内的具体工作者(如 no00)。组内的消息会分发给不同的消费者。
  5. PEL (Pending Entries List) : 这是 Redis Stream 最重要的机制之一。它记录了"已被组读取但尚未被确认(ACK)"的消息。这保证了如果消费者挂了,消息不会丢,可以重试。

2. 操作逻辑与核心指令

我们将操作分为三个阶段:生产(写入)消费(读取)维护(确认与监控)

2.1 生产消息:XADD

生产者向 Stream 写入数据。
指令格式:

bash 复制代码
XADD key [ID] field value [field value ...]
  • key: Stream 的名字。
  • ID: 消息 ID。通常使用 *,让 Redis 自动生成唯一 ID。
  • field value: 消息内容,类似于 Hash 结构。
    示例:
bash 复制代码
XADD file_cmd_message_key * device_id 001 command upload
# 返回: "1715000000000-0"

2.2 消费消息:XREADGROUP (核心逻辑)

消费者通过消费组读取消息。这是最复杂的部分,请结合下面的序列图理解。
指令格式:

bash 复制代码
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] STREAMS key ID
  • GROUP group consumer: 指定组名和消费者名。
  • COUNT count: 一次最多读多少条。
  • BLOCK milliseconds: 阻塞等待毫秒数(实现长轮询)。
  • ID:
    • >: 只读取从未被该组消费过 的新消息(最常用)。
    • 0: 读取从最早开始的所有历史消息。
    • 其他 ID: 从该 ID 之后开始读。
      消费逻辑流程图:

Pending List (PEL) Redis Stream (file_cmd_message_key) Consumer Group (FILE_CMD_MESSAGE_GROUP) Java Application Pending List (PEL) Redis Stream (file_cmd_message_key) Consumer Group (FILE_CMD_MESSAGE_GROUP) Java Application 阶段1: 发送读取请求 阶段2: 投递与进入 PEL 阶段3: 业务处理 消息消费闭环完成 Entry A 滞留在 PEL 中 等待其他消费者 XCLAIM 重试 alt [处理成功] [处理失败/未 ACK] XREADGROUP GROUP ... STREAMS key > 查找 ID > LastDeliveredID 的消息 读取新消息 (Entry A) 将 Entry A 加入 PEL (状态: Pending) 更新 LastDeliveredID 返回 Entry A onMessage(Entry A) 处理业务... XACK key group EntryA_ID 从 PEL 中移除 Entry A

2.3 消息确认:XACK

指令格式:

bash 复制代码
XACK key group id [id ...]
  • 告诉 Redis,这条消息我已经处理完了,从 PEL 中移除。

2.4 状态监控:XINFO (排查神器)

你之前排查问题用的就是这个,它能告诉你 Stream 的内部状态。
指令格式:

  • XINFO STREAM key: 查看 Stream 长度、首尾 ID。
  • XINFO GROUPS key: 查看有多少个消费组。
  • XINFO CONSUMERS key group: 查看组下有哪些活跃的消费者(如果 Java 连上了,这里会有名字)。
  • XPENDING key group: 查看 PEL 列表(有哪些消息卡住了)。

3. Spring Boot 集成实战

Spring Data Redis 提供了两套 API:

  1. 命令式 API (RedisTemplate): 灵活,适合手动控制。
  2. 容器式 API (StreamMessageListenerContainer) : 声明式,自动后台监听,适合生产环境

3.1 整体集成架构图

Redis Server
Spring Boot 应用
Spring Boot 应用
消费者模块
生产者模块
XADD
发送消息
XREADGROUP
投递消息
回调
处理成功
XACK
Service / Controller
RedisTemplate.opsForStream
StreamMessageListenerContainer
StreamListener.onMessage
Stream Key
Consumer Group

3.2 依赖引入

确保 pom.xml 中有:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.3 生产者代码 (使用 RedisTemplate)

java 复制代码
@Autowired
private StringRedisTemplate redisTemplate;
public void sendMessage(String key, Map<String, String> message) {
    // 记录 ID
    RecordId recordId = redisTemplate.opsForStream()
        .add(Record.of(message).withStreamKey(key));
    System.out.println("消息发送成功,ID: " + recordId.getValue());
}

3.4 消费者代码 (使用 StreamMessageListenerContainer)

这是你之前调试的核心部分。容器的逻辑是:封装了 XREADGROUP 循环,帮你处理连接、断线重连、回调等。
关键步骤:

  1. 创建 Stream 配置选项。
  2. 创建容器。
  3. 注册监听器 (receive)。
  4. 启动容器 (start)
java 复制代码
@Configuration
public class StreamConfig {
    @Bean
    public StreamMessageListenerContainer<String, MapRecord<String, String, String>> container(
            RedisConnectionFactory factory) {
        
        // 1. 配置容器选项 (比如默认每秒拉取一次,或者配置 poll-timeout)
        StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =
            StreamMessageListenerContainerOptions
                .builder()
                // 这里可以配置 pollTimeout, 批次大小等
                .build();
        // 2. 创建容器
        StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = 
            StreamMessageListenerContainer.create(factory, options);
        // 3. 定义 Stream 和 Group
        String streamKey = "file_cmd_message_key";
        String groupName = "FILE_CMD_MESSAGE_GROUP";
        
        // 定义消费者 (自动注册)
        Consumer consumer = Consumer.from(groupName, "consumer-01");
        // 定义监听器
        StreamListener<String, MapRecord<String, String, String>> listener = message -> {
            System.out.println("收到消息: " + message.getValue());
            // 处理业务逻辑...
            // 注意:如果不抛异常,Spring 默认会在方法返回后自动 XACK
        };
        // 4. 注册订阅 (从最新消息开始读: ReadOffset.lastConsumed())
        // StreamOffset.create(key, ReadOffset.lastConsumed()) 对应 Redis 命令中的 ID: >
        container.receive(
            consumer, 
            StreamOffset.create(streamKey, ReadOffset.lastConsumed()), 
            listener
        );
        // 5. 启动容器 (非常重要!不启动啥都不会发生)
        container.start();
        return container;
    }
}

3.5 集成中的关键点 (避坑指南)

  1. 确保 Group 存在 :
    Spring 的 receive 方法不会 自动创建 Group。在应用启动前,必须确保执行了 XGROUP CREATE(或者代码中通过 redisTemplate.opsForStream().createGroup 创建)。

    bash 复制代码
    XGROUP CREATE file_cmd_message_key FILE_CMD_MESSAGE_GROUP 0 MKSTREAM

    建议:使用 MKSTREAM,即使 Stream 没数据也能先建好组。

  2. 一定要调用 start() :
    容器是懒加载的,调用 start() 后台线程才会启动,开始发 XREADGROUP 命令。你可以通过 container.isRunning() 检查状态。

  3. 序列化问题 :
    使用 StringRedisTemplate 可以避免大部分序列化乱码问题。如果你的 Value 是 JSON,推荐序列化成 String 存入。


4. 总结与排查清单

当你遇到 Stream 相关问题时,按以下思路排查:

  1. 数据有进来了吗?
    • 命令:XLEN key / XRANGE key - +
    • 检查生产者是否正常。
  2. Group 创建了吗?
    • 命令:XINFO GROUPS key
    • 检查初始化脚本或代码逻辑。
  3. 消费者注册了吗?
    • 命令:XINFO CONSUMERS key group
    • 如果这里为空 -> Java 程序没发请求 -> 检查 container.start() 是否调用,检查是否连错 Redis 库。
  4. 消息卡住了吗?
    • 命令:XPENDING key group
    • 如果数量很大 -> 消费者处理慢或抛异常没 ACK
  5. 想手动干预?
    • 读不到:用 XADD 造数据,用 XREADGROUP ... 0 读历史。
    • 卡住了:用 XACK 手动确认,或用 XCLAIM 转移给别的消费者。
相关推荐
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.2 小时前
HAProxy四层负载实战:MariaDB高可用方案
数据库·mariadb
xixi_6662 小时前
mysql 的分组函数 ROLLUP 语法
数据库·mysql
范纹杉想快点毕业2 小时前
嵌入式通信协议深度解析:从SPI/I2C到CAN总线的完整实现指南嵌入式工程师的炼成之路:从校园到实战的跨越
linux·运维·服务器·数据库·算法
数据知道2 小时前
PostgreSQL 实战:如何优雅高效地进行全文检索
大数据·数据库·postgresql·全文检索
山峰哥2 小时前
SQL调优实战:从索引到执行计划的深度优化指南
大数据·开发语言·数据库·sql·编辑器·深度优先
心枢AI研习社2 小时前
数据库系列3——条件查询:把数据“筛对、排对”(WHERE/逻辑/范围/null/LIKE 一次讲透)
数据库·人工智能·oracle·aigc
heze092 小时前
sqli-labs-Less-26a
数据库·mysql·网络安全
橘子132 小时前
MySQL表的内外连接(九)
数据库·mysql·adb
Geoking.2 小时前
【Redis】Redis 中的 Pipeline 与 Lua 脚本:高性能与原子性的两种武器
redis·lua