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
核心概念解析:
- Stream (流) : 一个 Key,对应一个消息队列。比如
file_cmd_message_key。 - Entry (消息) : 包含一个唯一的 ID 和一组键值对。ID 通常由 Redis 自动生成(格式:
timestamp-sequence,如1700000000000-0)。 - Consumer Group (消费组) : 用于实现发布/订阅 模式下的负载均衡。一个 Stream 可以有多个组,每个组独立消费消息(互不干扰)。
- Consumer (消费者) : 组内的具体工作者(如
no00)。组内的消息会分发给不同的消费者。 - 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:
- 命令式 API (
RedisTemplate): 灵活,适合手动控制。 - 容器式 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 循环,帮你处理连接、断线重连、回调等。
关键步骤:
- 创建 Stream 配置选项。
- 创建容器。
- 注册监听器 (
receive)。 - 启动容器 (
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 集成中的关键点 (避坑指南)
-
确保 Group 存在 :
Spring 的receive方法不会 自动创建 Group。在应用启动前,必须确保执行了XGROUP CREATE(或者代码中通过redisTemplate.opsForStream().createGroup创建)。bashXGROUP CREATE file_cmd_message_key FILE_CMD_MESSAGE_GROUP 0 MKSTREAM建议:使用
MKSTREAM,即使 Stream 没数据也能先建好组。 -
一定要调用
start():
容器是懒加载的,调用start()后台线程才会启动,开始发XREADGROUP命令。你可以通过container.isRunning()检查状态。 -
序列化问题 :
使用StringRedisTemplate可以避免大部分序列化乱码问题。如果你的 Value 是 JSON,推荐序列化成 String 存入。
4. 总结与排查清单
当你遇到 Stream 相关问题时,按以下思路排查:
- 数据有进来了吗?
- 命令:
XLEN key/XRANGE key - + - 检查生产者是否正常。
- 命令:
- Group 创建了吗?
- 命令:
XINFO GROUPS key - 检查初始化脚本或代码逻辑。
- 命令:
- 消费者注册了吗?
- 命令:
XINFO CONSUMERS key group - 如果这里为空 -> Java 程序没发请求 -> 检查
container.start()是否调用,检查是否连错 Redis 库。
- 命令:
- 消息卡住了吗?
- 命令:
XPENDING key group - 如果数量很大 -> 消费者处理慢或抛异常没 ACK。
- 命令:
- 想手动干预?
- 读不到:用
XADD造数据,用XREADGROUP ... 0读历史。 - 卡住了:用
XACK手动确认,或用XCLAIM转移给别的消费者。
- 读不到:用