一、Redis的Stream介绍
1.1 Redis Stream 是什么?
Redis Stream 是 Redis 5.0 版本引入的一种持久化的、有序的、可追溯的消息队列数据结构,专门用于处理消息发布 / 订阅(Pub/Sub)场景中消息丢失、无法回溯、不支持多消费者组等问题。
你可以把它理解成一个无限延伸的消息日志:
- 每条消息都有一个唯一的
ID(格式:时间戳-序列号,如1710000000000-0) - 消息按 ID 从小到大有序排列
- 支持多消费者组同时消费,且每个消费者组有独立的消费进度
- 消息持久化存储,重启 Redis 后数据不丢失
1.2 Stream 的核心特性与核心概念
1.2.1 核心概念
| 概念 | 说明 |
|---|---|
| 消息 ID | 自动生成(XADD 命令)或手动指定,由毫秒时间戳 + 序列号组成,保证全局唯一且有序 |
| 消费者组(Consumer Group) | 一组消费者的集合,每个组有独立的消费游标(last-delivered-id),互不干扰 |
| 消费者(Consumer) | 隶属于某个消费组的具体消费实例,有唯一名称 |
| Pending 列表(PEL) | 记录已分发给消费者但未确认的消息,防止消息丢失 |
1.2.2 核心命令(新手必知)
下面是最常用的 Stream 命令,附带简单示例:
(1)添加消息(XADD)
向 Stream 中添加消息,是最基础的写入操作:
redis
# 语法:XADD key [NOMKSTREAM] [MAXLEN | MINID [= | ~] threshold [LIMIT count]] *|id field value [field value ...]
# 示例:向 stream_myqueue 中添加一条消息,ID 由 Redis 自动生成
127.0.0.1:6379> XADD stream_myqueue * name "Alice" age 25
"1710000000000-0" # 返回自动生成的消息 ID
# 手动指定 ID(需大于已存在的最大 ID)
127.0.0.1:6379> XADD stream_myqueue 1710000000000-1 name "Bob" age 30
"1710000000000-1"
(2)读取消息(XREAD)
非阻塞 / 阻塞式读取消息(独立消费,不依赖消费组):
redis
# 语法:XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
# 示例1:非阻塞读取,从 ID 0-0 开始读取最多 2 条消息
127.0.0.1:6379> XREAD COUNT 2 STREAMS stream_myqueue 0-0
1) 1) "stream_myqueue"
2) 1) 1) "1710000000000-0"
2) 1) "name"
2) "Alice"
3) "age"
4) "25"
2) 1) "1710000000000-1"
2) 1) "name"
2) "Bob"
3) "age"
4) "30"
# 示例2:阻塞读取(BLOCK 0 表示永久阻塞),从最新 ID 开始等待新消息
127.0.0.1:6379> XREAD BLOCK 0 STREAMS stream_myqueue $
# 此时会阻塞,直到有新消息写入后返回
(3)创建消费组(XGROUP CREATE)
为 Stream 创建消费组(多消费者协作的基础):
redis
# 语法:XGROUP [CREATE key group id [MKSTREAM]] [SETID key group id] [DESTROY key group] [DELCONSUMER key group consumer]
# 示例:创建消费组 group1,从 ID 0-0 开始消费(从头消费)
127.0.0.1:6379> XGROUP CREATE stream_myqueue group1 0-0 MKSTREAM
OK
# 若想从最新消息开始消费,ID 用 $
127.0.0.1:6379> XGROUP CREATE stream_myqueue group2 $ MKSTREAM
OK
(4)消费组读取消息(XREADGROUP)
消费组内的消费者读取消息:
redis
# 语法:XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] id [id ...]
# 示例:消费者 consumer1 从 group1 中读取未消费的消息(> 表示读取消费组游标之后的消息)
127.0.0.1:6379> XREADGROUP GROUP group1 consumer1 COUNT 1 STREAMS stream_myqueue >
1) 1) "stream_myqueue"
2) 1) 1) "1710000000000-0"
2) 1) "name"
2) "Alice"
3) "age"
4) "25"
(5)确认消息(XACK)
消费完成后确认消息,从 PEL 列表中移除:
redis
# 语法:XACK key group id [id ...]
# 示例:确认 group1 中 ID 为 1710000000000-0 的消息已消费完成
127.0.0.1:6379> XACK stream_myqueue group1 1710000000000-0
(integer) 1 # 成功确认的消息数
1.3 Stream 的典型使用场景
- 消息队列:替代传统的 Pub/Sub,支持消息持久化、回溯、多消费组,适合订单通知、日志收集等场景。
- 事件溯源:按时间顺序记录系统事件,可追溯任意时间点的状态。
- 实时数据流:结合 Redis 的高性能,处理实时产生的数据流(如物联网设备数据)。
1.4 与传统Redis pub/sub区别
1.5 总结
- Redis Stream 是 Redis 5.0 推出的持久化有序消息队列,解决了传统 Pub/Sub 消息丢失、无法回溯的问题。
- 核心特性包括:唯一消息 ID、多消费组、PEL 未确认消息列表、阻塞 / 非阻塞读取。
- 核心命令:
XADD(写消息)、XREAD(独立读)、XGROUP(消费组管理)、XREADGROUP(消费组读)、XACK(确认消息)。
二、Java实现
2.1 依赖配置
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
2.2 自定义注解
import java.lang.annotation.*;
/**
* Redis Stream 消息监听注解
* 标记在方法上,用于自动注册 Stream 消费者
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface StreamListener {
/**
* Stream 名称(必填)
*/
String streamKey();
/**
* 消费组名称(默认:stream_group_ + streamKey)
*/
String groupName() default "";
/**
* 消费者名称(默认:stream_consumer_ + 随机数)
*/
String consumerName() default "";
/**
* 每次拉取的消息数量
*/
int batchSize() default 1;
/**
* 阻塞等待时间(毫秒,0 表示永久阻塞)
*/
long blockTime() default 0;
}
2.3 RedisTemplate配置(序列化+Stream配置)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* RedisTemplate 配置:指定序列化方式,保证 Stream 消息的序列化/反序列化正常
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// String 序列化器(key/HashKey 使用)
StringRedisSerializer stringSerializer = new StringRedisSerializer();
// JSON 序列化器(value/HashValue 使用)
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
// 设置 Key 序列化
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
// 设置 Value 序列化
redisTemplate.setValueSerializer(jsonSerializer);
redisTemplate.setHashValueSerializer(jsonSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
2.4 通用消息发布器(基于RedisTemplate)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* 基于 RedisTemplate 的 Stream 通用发布器
*/
@Component
public class RedisStreamPublisher {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 发布任意类型消息到 Stream
* @param streamKey Stream 名称
* @param message 消息体(任意对象)
* @return 消息 ID(格式:时间戳-序列号)
*/
public <T> String publish(String streamKey, T message) {
try {
// 构建 Stream 消息记录(自动生成 ID)
ObjectRecord<String, T> record = StreamRecords.newRecord()
.in(streamKey) // 指定 Stream 名称
.ofObject(message); // 消息体
// 发送消息并获取自动生成的 ID
String messageId = redisTemplate.opsForStream().add(record);
System.out.println("消息发布成功,StreamKey:" + streamKey + ",消息ID:" + messageId);
return messageId;
} catch (Exception e) {
throw new RuntimeException("发布 Stream 消息失败", e);
}
}
}
2.5 通用消息订阅处理器(基于RedisTemplate)
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.stream.StreamListener;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 基于 RedisTemplate 的 Stream 订阅管理器
* 扫描 @StreamListener 注解,自动创建消费组和消费者
*/
@Component
public class RedisStreamSubscriber implements ApplicationContextAware {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private ApplicationContext applicationContext;
/**
* 初始化:扫描所有 @StreamListener 注解的方法,启动消费者
*/
@PostConstruct
public void init() {
// 扫描所有带有 @StreamListener 注解的 Bean
Map<String, Object> beans = applicationContext.getBeansWithAnnotation(StreamListener.class);
for (Object bean : beans.values()) {
for (Method method : bean.getClass().getDeclaredMethods()) {
StreamListener annotation = method.getAnnotation(StreamListener.class);
if (annotation != null) {
// 为每个注解方法启动独立的消费者
startConsumer(bean, method, annotation);
}
}
}
}
/**
* 启动单个消费者
* @param bean 注解所在的 Bean 实例
* @param method 注解标记的处理方法
* @param annotation 注解参数
*/
private void startConsumer(Object bean, Method method, StreamListener annotation) {
// 解析注解参数
String streamKey = annotation.streamKey();
String groupName = annotation.groupName().isEmpty() ? "stream_group_" + streamKey : annotation.groupName();
String consumerName = annotation.consumerName().isEmpty() ? "stream_consumer_" + UUID.randomUUID().toString().substring(0, 8) : annotation.consumerName();
int batchSize = annotation.batchSize();
long blockTime = annotation.blockTime();
// 1. 创建消费组(不存在则创建)
createConsumerGroupIfNotExists(streamKey, groupName);
// 2. 配置消费者容器(核心:Spring Data Redis 提供的消息监听容器)
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, Object>> options =
StreamMessageListenerContainer.StreamMessageListenerContainerOptions
.builder()
.batchSize(batchSize) // 每次拉取的消息数
.pollTimeout(Duration.ofMillis(blockTime)) // 阻塞等待时间(0 表示永久阻塞)
.targetType(HashMap.class) // 消息体类型
.build();
// 3. 创建并启动监听容器
StreamMessageListenerContainer<String, MapRecord<String, String, Object>> container =
StreamMessageListenerContainer.create(redisTemplate.getConnectionFactory(), options);
// 4. 注册消息监听器
container.receive(
Consumer.from(groupName, consumerName), // 消费组 + 消费者
StreamOffset.create(streamKey, ReadOffset.lastConsumed()),// 从上次消费的位置开始读取
new org.springframework.data.redis.stream.StreamListener<>() {
@Override
public void onMessage(MapRecord<String, String, Object> record) {
try {
// 获取消息体和消息 ID
String messageId = record.getId().getValue();
Map<String, Object> payload = record.getValue();
// 反序列化消息体为目标类型(方法的第一个参数类型)
Class<?> paramType = method.getParameterTypes()[0];
Object message = redisTemplate.getValueSerializer().deserialize(
redisTemplate.getValueSerializer().serialize(payload.get("__obj"))
);
// 调用注解标记的处理方法
method.invoke(bean, message);
// 5. 确认消息消费完成(ACK)
redisTemplate.opsForStream().acknowledge(streamKey, groupName, record.getId());
System.out.println("消息消费成功,ID:" + messageId + ",内容:" + message);
} catch (Exception e) {
System.err.println("处理 Stream 消息失败:" + e.getMessage());
// 可扩展:重试逻辑、死信队列等
}
}
}
);
// 启动容器
container.start();
System.out.println("Stream 消费者启动成功:streamKey=" + streamKey + ", group=" + groupName + ", consumer=" + consumerName);
}
/**
* 创建消费组(不存在则创建)
*/
private void createConsumerGroupIfNotExists(String streamKey, String groupName) {
try {
// 尝试创建消费组(从 Stream 头部开始消费)
redisTemplate.opsForStream().createGroup(streamKey, ReadOffset.from("0-0"), groupName);
} catch (Exception e) {
// 异常通常是消费组已存在,忽略即可
System.out.println("消费组 " + groupName + " 已存在,无需重复创建:" + e.getMessage());
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
2.6 使用示例
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@SpringBootApplication
public class RedisStreamApplication {
public static void main(String[] args) {
SpringApplication.run(RedisStreamApplication.class, args);
}
// 示例消息实体
static class OrderMessage {
private String orderId;
private String userId;
private double amount;
// 必须有默认构造函数(序列化需要)
public OrderMessage() {}
public OrderMessage(String orderId, String userId, double amount) {
this.orderId = orderId;
this.userId = userId;
this.amount = amount;
}
// getter/setter
public String getOrderId() { return orderId; }
public void setOrderId(String orderId) { this.orderId = orderId; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public double getAmount() { return amount; }
public void setAmount(double amount) { this.amount = amount; }
@Override
public String toString() {
return "OrderMessage{" +
"orderId='" + orderId + '\'' +
", userId='" + userId + '\'' +
", amount=" + amount +
'}';
}
}
// 消息发布示例
@Component
static class OrderService {
@Autowired
private RedisStreamPublisher publisher;
// 测试发布消息
public void createOrder() {
OrderMessage orderMsg = new OrderMessage("ORDER_789", "USER_123", 199.9);
publisher.publish("stream_order", orderMsg);
}
}
// 消息消费示例(注解标记)
@Component
static class OrderMessageHandler {
@StreamListener(
streamKey = "stream_order",
groupName = "order_group",
batchSize = 2,
blockTime = 0
)
public void handleOrderMessage(OrderMessage orderMessage) {
// 业务处理逻辑
System.out.println("【RedisTemplate 消费】收到订单消息:" + orderMessage);
}
}
}
2.7 配置文件(application.yml)
spring:
data:
redis:
host: localhost
port: 6379
password: # 无密码留空
database: 0
timeout: 10000

664

被折叠的 条评论
为什么被折叠?



