一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第N天,点击查看活动详情。
Redis Stream
Redis Stream 是 Redis 5.0 版本新增加的数据结构。
Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。
简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。
而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
Redis Stream 的结构如下所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容:
每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用 xadd 指令追加消息时自动创建。
上图解析:
- Consumer Group :消费组,使用 XGROUP CREATE 命令创建,一个消费组有多个消费者(Consumer)。
- last_delivered_id :游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
- pending_ids :消费者(Consumer)的状态变量,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符)。
消息队列相关命令:
- XADD - 添加消息到末尾
- XTRIM - 对流进行修剪,限制长度
- XDEL - 删除消息
- XLEN - 获取流包含的元素数量,即消息长度
- XRANGE - 获取消息列表,会自动过滤已经删除的消息
- XREVRANGE - 反向获取消息列表,ID 从大到小
- XREAD - 以阻塞或非阻塞方式获取消息列表
消费者组相关命令:
- XGROUP CREATE - 创建消费者组
- XREADGROUP GROUP - 读取消费者组中的消息
- XACK - 将消息标记为"已处理"
- XGROUP SETID - 为消费者组设置新的最后递送消息ID
- XGROUP DELCONSUMER - 删除消费者
- XGROUP DESTROY - 删除消费者组
- XPENDING - 显示待处理消息的相关信息
- XCLAIM - 转移消息的归属权
- XINFO - 查看流和消费者组的相关信息;
- XINFO GROUPS - 打印消费者组的信息;
- XINFO STREAM - 打印流信息
XADD
使用 XADD 向队列添加消息,如果指定的队列不存在,则创建一个队列,XADD 语法格式:
XADD key ID field value [field value ...]
- key :队列名称,如果不存在就创建
- ID :消息 id,我们使用 * 表示由 redis 生成,可以自定义,但是要自己保证递增性。
- field value : 记录。
XDEL
使用 XDEL 删除消息,语法格式:
XDEL key ID [ID ...]
- key:队列名称
- ID :消息 ID
XLEN
使用 XLEN 获取流包含的元素数量,即消息长度,语法格式:
XLEN key
- key:队列名称
XRANGE
使用 XRANGE 获取消息列表,会自动过滤已经删除的消息 ,语法格式:
XRANGE key start end [COUNT count]
- key :队列名
- start :开始值, - 表示最小值
- end :结束值, + 表示最大值
- count :数量
XREVRANGE
使用 XREVRANGE 获取消息列表,会自动过滤已经删除的消息 ,语法格式:
XREVRANGE key end start [COUNT count]
- key :队列名
- end :结束值, + 表示最大值
- start :开始值, - 表示最小值
- count :数量
XREAD
使用 XREAD 以阻塞或非阻塞方式获取消息列表 ,语法格式:
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
- count :数量
- milliseconds :可选,阻塞毫秒数,没有设置就是非阻塞模式
- key :队列名
- id :消息 ID
XGROUP CREATE
使用 XGROUP CREATE 创建消费者组,语法格式:
XGROUP [CREATE key groupname id-or-$] [SETID key groupname id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername]
- key :队列名称,如果不存在就创建
- groupname :组名。
- $ : 表示从尾部开始消费,只接受新消息,当前 Stream 消息会全部忽略。
XREADGROUP GROUP
使用 XREADGROUP GROUP 读取消费组中的消息,语法格式:
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
- group :消费组名
- consumer :消费者名。
- count : 读取数量。
- milliseconds : 阻塞毫秒数。
- key : 队列名。
- ID : 消息 ID。
在 SpringBoot 中使用 Redis Stream
Redis 配置
* @author: xx
* @date: 2022/4/13 9:38
* @description: redis 配置类 修改序列化方式
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
}
封装 Redis Stream 工具类
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisTemplate;
import java.time.Duration;
import java.util.List;
import java.util.Map;
public class RedisStreamUtil {
private RedisTemplate<String, Object> redisTemplate;
public RedisStreamUtil(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* @author: xx
* @date: 2022/4/12 17:29
* @description: 创建分组
* streamName: 流名称
* groupName: 分组名称
* readOffset: 消息读取规则
*/
public String addGroup(String streamName, String groupName, ReadOffset readOffset) {
String group = null;
//获取是否已有该名称 stream
Long size = redisTemplate.opsForStream().size(streamName);
if (size == null || size < 1) {
// 没有设置读取规则
group = redisTemplate.opsForStream().createGroup(streamName,readOffset, groupName);
}
if (size != null && size >= 1) {
// 已有直接创建新的分组
group = redisTemplate.opsForStream().createGroup(streamName, groupName);
}
return group;
}
/**
* @author: xx
* @date: 2022/4/12 17:29
* @description: 发消息
* streamName: 流名称
* map: 用来封装消息内容
* RecordId: 返回消息id
*/
public RecordId addMessage(String streamName, Map<Object, Object> map) {
return redisTemplate.opsForStream().add(streamName, map);
}
/**
* @author: xx
* @date: 2022/4/12 17:30
* @description: 收消息 每次读取一条消息
* streamName: 流名称
* groupName: 分组名称
* consumerName: 消费者名称 (没有会自动创建)
* duration: 读不到消息的阻塞时间
* readOffset: 消息读取规则
*
*/
public MapRecord<String, Object, Object> readOne(String streamName,String groupName,String consumerName,Duration duration, ReadOffset readOffset) {
List<MapRecord<String, Object, Object>> recordList = redisTemplate.opsForStream().read(
// 创建消费者
Consumer.from(groupName, consumerName),
// 每次读取 1条消息
StreamReadOptions.empty().count(1).block(duration),
StreamOffset.create(streamName, readOffset)
);
if (recordList == null || recordList.isEmpty()) {
return null;
}
return recordList.get(0);
}
/**
* @author: xx
* @date: 2022/4/13 8:52
* @description: 手动消息确认
* streamName: 流名称
* groupName: 分组名称
* record: 消息
*/
public Long acknowledge(String streamName,String groupName, MapRecord<String, Object, Object> record) {
return redisTemplate.opsForStream().acknowledge(streamName, groupName, record.getId());
}
/**
* @author: xx
* @date: 2022/4/13 9:10
* @description: 删除消息
* streamName: 流名称
* record: 消息
*/
public Long delete(String streamName, MapRecord<String, Object, Object> record) {
return redisTemplate.opsForStream().delete(streamName, record.getId());
}
}
使用 Redis Stream 测试
@RestController
@Slf4j
public class RedisController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* @author: xx
* @date: 2022/4/13 9:06
* @description: 创建分组
*/
@GetMapping("/test1")
public Object test1() {
RedisStreamUtil streamUtil = new RedisStreamUtil(redisTemplate);
String group = streamUtil.addGroup("stream1", "g1", ReadOffset.from("0"));
log.info("group:{}",group);
return "ok";
}
/**
* @author: xx
* @date: 2022/4/13 9:06
* @description: 发送消息
*/
@GetMapping("/test2")
public Object test2(String key,String value) {
Map<Object, Object> map = new HashMap<>();
map.put(key,value);
RedisStreamUtil streamUtil = new RedisStreamUtil(redisTemplate);
RecordId recordId = streamUtil.addMessage("stream1", map);
log.info("recordId:{}",recordId);
return recordId;
}
/**
* @author: xx
* @date: 2022/4/13 9:07
* @description: 接收消息
*/
@GetMapping("/test3")
public Object test3() {
RedisStreamUtil streamUtil = new RedisStreamUtil(redisTemplate);
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
MapRecord<String, Object, Object> record = streamUtil.readOne("stream1", "g1", "c1", Duration.ofSeconds(2), ReadOffset.lastConsumed());
if (record == null) {
log.info("没有最新消息");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
Map<Object, Object> map = record.getValue();
log.info("最新消息:{}",map);
streamUtil.acknowledge("stream1","g1",record);
streamUtil.delete("stream1",record);
}
}
}).start();
return "ok";
}
}
使用效果如下:
消费完消息需要确认消息,没有确认的消息会保存在pedding-list中,可以被再次消费,也会多占用内存.