Redis Stream
接上篇文章,这章说说消息队列。消息队列用于应用间通信,“消息”是两个应用间传输的数据单位。今天介绍一款轻量级、用于小型项目间、相对可靠的队列——redis stream
消息队列
当我们使用消息队列时,希望消息队列具有以下优点:
- 支持发布/订阅模式
- 消息持久化,做到消息宕机不丢失
- 可重复消费
- 可阻塞式拉取
- 处理消息消费失败的机制
发布/订阅和Stream的区别
redis做消息队列有多种方式,其中发布/订阅就是其中一种。发布/订阅有什么缺点呢?
发布/订阅可以做到消息的广播模式,对消费者能做到有消息就通知的能力,但有个致命缺点,即发布/订阅不能持久化消息。当redis服务出现断开、宕机等突发情况,消息就会丢失。
Stream
使用Stream数据结构,需要注意redis的版本。redis从5.0开始支持Stream能力,是对消息队列(Message Queue)的完善实现
对于队列,我们要考虑的是除了发布/订阅模式的不能持久化的缺点之外,我们还需要考虑
- 消息的生产
- 消息的消费
- 单播和多播
- 阻塞和非阻塞
- 消息的有序性
- 消息的重复消费
- 消息堆积等
Stream的结构
每个stream数据都有唯一的名称,那就是redis的key,不能显示创建stream,但可以在我们首次使用XADD指令时隐式创建stream以及指定key。
-
stream中每一个消息都有一个唯一ID,用于区分不同消息。在stream中,entry是消息的称呼,即ID也叫entry的id
entry ID是通过XADD指令返回的,ID的结构是
<millisecondsTime>-<sequenceNumber>
-
consumer group:消费者组,使用XGROUP CREATE指令创建,一个消费者组有多个消费者,组内的消费者之间是竞争关系。
-
last_delivered_id:游标。每个消费者组有一个游标,表示消费者组读取消息的位置
-
pending_ids:待确认消息id集合。属于消费者的属性,当中记录当前消费者读取但未确认(ACK)的消息id
Stream相关操作
[官方链接](XADD | Docs (redis.io))
属于Stream本身的操作:
- XADD:添加消息到指定Stream中,如果Stream不存在,则创建
- XDEL:从Stream中删除指定的条目,并返回删除的条目数
- XINFO STREAM:提供Stream信息
- XLEN:返回Stream长度,不存在返回0
- XRANGE:范围查看Stream中的条目数据
- XREAD:查看指定的条目数据
- XREVRANGE:和XRANGE相反,以反方向顺序查看条目数据
- XTRIM:通过删除就条目来修剪Stream
属于消费者组或消费者的操作:
- XGROUP CREATE:给Stream创建一个消费者组
- XGROUP CREATECONSUMER:给Stream中的消费者组创建一个消费者
- XGROUP DELCONSUMER:删除一个消费者
- XGROUP DESTROY:完全销毁消费者组,包括消费者组、消费者、消费者的pending、last_deliver_id等
- XGROUP SETID:为消费者组设置last delivered ID
- XINFO GROUPS:提供消费者组信息
- XINFO CONSUMERS:提供消费者信息
- XPENDING:查看消费者组中读取但未确认的消息集合
- XREADGROUP:和XREAD类似,XREADGROUP支持消费者组
项目中应用
spring启动时初始化stream
/**
* @author zhangyi
*/
@Component
@Slf4j
@Order(1)
public class MqRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 初始化stream
MqService mqService = SpringUtils.getBean(MqService.class);
mqService.init();
// 添加消费者监听
StreamConsumerService consumerService = SpringUtils.getBean(StreamConsumerService.class);
consumerService.startListenering();
}
}
@Override
public void init() {
log.info("init redis stream");
try {
StreamInfo.XInfoStream streamInfo = redisUtils.streamInfo(BIG_MODEL_STREAM_KEY);
} catch (Exception e) {
// 还没创建stream时
if (e.getMessage().contains("ERR no such key")) {
redisUtils.sendMessageByStream(BIG_MODEL_STREAM_KEY, ImmutableMap.of("init", "init"));
// 还没创建group和consumer时
if (redisUtils.groupInfo(BIG_MODEL_STREAM_KEY).stream().noneMatch(group -> Objects.equals(BIG_MODEL_STREAM_CONSUMER_GROUP_NAME, group.groupName()))) {
redisUtils.createStreamGroup(BIG_MODEL_STREAM_KEY, BIG_MODEL_STREAM_CONSUMER_GROUP_NAME);
}
}
}
log.info("init redis stream success");
}
/**
* @author zhangyi
*/
@Slf4j
@Component
public class StreamConsumerServiceImpl implements StreamConsumerService {
@Autowired
private RedisUtils redisUtils;
@Override
public void startListenering() {
AsyncManager.me().execute(new TimerTask() {
@Override
public void run() {
while (true) {
try {
// 拉取并处理消息
pullFromStreamAndConsume();
} catch (Exception e) {
log.error(e.getMessage(), e);
ThreadUtil.sleep(5, TimeUnit.MINUTES);
}
}
}
});
}
private void pullFromStreamAndConsume() {
List<MapRecord<String, String, String>> records = redisUtils.xReadGroup(BIG_MODEL_STREAM_KEY, BIG_MODEL_STREAM_CONSUMER_GROUP_NAME, BIG_MODEL_STREAM_CONSUMER_1_NAME);
for (MapRecord<String, String, String> record : records) {
RecordId recordId = record.getId();
Map<String, String> recordMap = record.getValue();
for (Map.Entry<String, String> entry : recordMap.entrySet()) {
String value = JsonUtils.fromJson(entry.getValue(), String.class);
if (Objects.equals("init", entry.getKey()) && Objects.equals("init", value)) {
redisUtils.xAck(BIG_MODEL_STREAM_KEY, BIG_MODEL_STREAM_CONSUMER_GROUP_NAME, recordId.getValue());
log.info("消费者消费了一条消息,recordId:{}", recordId.getValue());
log.info("field:{}", entry.getKey());
log.info("value:{}", value);
continue;
}
try {
// 处理消息
doSomething();
redisUtils.xAck(BIG_MODEL_STREAM_KEY, BIG_MODEL_STREAM_CONSUMER_GROUP_NAME, recordId.getValue());
log.info("消费者消费了一条消息,recordId:{}", recordId.getValue());
log.info("field:{}", entry.getKey());
log.info("value:{}", value);
} catch (Exception e) {
log.error("无法消费消息,recordId:{}", recordId.getValue());
log.error(e.getMessage(), e);
}
}
}
// records为空,休眠30s
if (records.isEmpty()) {
ThreadUtil.sleep(30, TimeUnit.SECONDS);
}
}
}
/**
* spring redis 工具类
*
* @author zhangyi
**/
@Component
public class RedisUtils {
@Autowired
public RedisTemplate redisTemplate;
/*
* ------------------------------------------------------redis stream------------------------------------------------------
*/
/**
* 获取名为key的stream相关信息
*
* @param key stream key
* @return info
*/
public StreamInfo.XInfoStream streamInfo(String key) {
return redisTemplate.opsForStream().info(key);
}
/**
* 获取名为key的stream上的消费者组
*
* @param key stream key
* @return XInfoGroups
*/
public StreamInfo.XInfoGroups groupInfo(String key) {
return redisTemplate.opsForStream().groups(key);
}
/**
* 给消息队列发送消息,相当于生产者
*
* @param key stream key
* @param message field value 对
* @return RecordId 消息id
*/
public RecordId sendMessageByStream(String key, Map<String, Object> message) {
return sendMessageByStream(key, message, 1000);
}
public RecordId sendMessageByStream(String key, Map<String, Object> message, int maxLength) {
Map<byte[], byte[]> messageMap = new HashMap<>();
for (Map.Entry<String, Object> entry : message.entrySet()) {
messageMap.put(entry.getKey().getBytes(), JsonUtils.toJson(entry.getValue()).getBytes());
}
MapRecord<byte[], byte[], byte[]> entries = StreamRecords.newRecord().in(key.getBytes()).ofMap(messageMap);
RedisStreamCommands.XAddOptions options = RedisStreamCommands.XAddOptions.maxlen(maxLength);
return (RecordId) redisTemplate.execute((RedisConnection connection) -> connection.xAdd(entries, options));
}
/**
* 完全销毁消费者组
*
* @param key stream key
* @param groupName 消费者组名称
*/
public void destroyGroup(String key, String groupName) {
redisTemplate.opsForStream().destroyGroup(key, groupName);
}
/**
* 创建名为key的stream的groupName的消费者组
*
* @param key stream key
* @param groupName 消费者组名称
*/
public void createStreamGroup(String key, String groupName) {
redisTemplate.opsForStream().createGroup(key, groupName);
}
/**
* 读取消息
*
* @param key stream key
* @param groupName 消费者组名称
* @param consumerName 消费者名称
* @return list
*/
public List<MapRecord<String, String, String>> xReadGroup(String key, String groupName, String consumerName) {
List<ByteRecord> records = (List<ByteRecord>) redisTemplate.execute((RedisConnection connection) ->
connection.xReadGroup(Consumer.from(groupName, consumerName), StreamReadOptions.empty().count(5),
StreamOffset.create(key.getBytes(), ReadOffset.lastConsumed())));
ArrayList<MapRecord<String, String, String>> list = new ArrayList<>(records.size());
for (ByteRecord record : records) {
StringRedisSerializer serializer = new StringRedisSerializer();
MapRecord<String, String, String> mapRecord = record.deserialize(serializer, serializer, serializer);
list.add(mapRecord);
}
return list;
}
/**
* XACK操作
*
* @param key stream key
* @param groupName 消费者组名称
* @param recordId recordId
*/
public void xAck(String key, String groupName, String recordId) {
redisTemplate.opsForStream().acknowledge(key, groupName, recordId);
}
}