本文将介绍springboot下监听redis的stream、创建消费组、删除消费组、以及pending队列监控和消息ack和删除
1 基础设施
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置文件
spring:
redis.:
host: 192.168.0.3
port: 6379
database: 0
timeout: 15000
lettuce:
pool:
max-idle: 50 # 连接池中的最大空闲连接
min-idle: 10 # 连接池中的最小空闲连接
max-active: 300 # 连接池的最大数据库连接数
max-wait: -1 #连接池最大阻塞等待时间
redis配置类
@Configuration
public class MyRedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(RedisSerializer.string());
// hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
2 封装消息和消息ID的对象
消息对象的创建:
使用 org.springframework.data.redis.connection.stream.StreamRecords
的静态方法来创建消息实例
一个stream消息有两个内容。可以理解为:一个是key,一个是value。
key和value都可以使用自定义的对象,字节,字符串来定义
ByteRecord rawBytes(Map<byte[], byte[]> raw)
ByteBufferRecord rawBuffer(Map<ByteBuffer, ByteBuffer> raw)
StringRecord string(Map<String, String> raw)
<S, K, V> MapRecord<S, K, V> mapBacked(Map<K, V> map)
<S, V> ObjectRecord<S, V> objectBacked(V value)
RecordBuilder<?> newRecord() // 通过builder方式来创建消息
RecordId 表示消息ID
一条消息的ID是唯一的。并且有2部分组成
// ----------- 读取ID属性的实例方法
// 是否是系统自动生成的
boolean shouldBeAutoGenerated();
// 获取原始的id字符串
String getValue();
// 获取序列号部分
long getSequence();
// 获取时间戳部分
long getTimestamp();
// ----------- 创建ID的静态方法
RecordId of(@Nullable String value)
RecordId of(long millisecondsTime, long sequenceNumber)
RecordId autoGenerate()
3 往Stream推送Map消息
3.1 使用RedisTemplate
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void send1() {
HashMap<Object, Object> map = new HashMap<>();
map.put("xxx", "xxx");
final MapRecord<String, Object, Object> record = StreamRecords.newRecord()
.in("mystream")
.ofMap(map)
.withId(RecordId.autoGenerate());
RecordId recordId = redisTemplate.opsForStream().add(record);
// 是否是自动生成的
boolean autoGenerated = recordId.shouldBeAutoGenerated();
// id值
String value = recordId.getValue();
// 序列号部分
long sequence = recordId.getSequence();
// 时间戳部分
long timestamp = recordId.getTimestamp();
}
public void send2() {
StringRecord record = StreamRecords.string(Collections.singletonMap("xxx", xxx))
.withStreamKey("mystream")
.withId(RecordId.autoGenerate());
RecordId recordId = redisTemplate.opsForStream().add(record);
// 是否是自动生成的
boolean autoGenerated = recordId.shouldBeAutoGenerated();
// id值
String value = recordId.getValue();
// 序列号部分
long sequence = recordId.getSequence();
// 时间戳部分
long timestamp = recordId.getTimestamp();
}
3.2 使用RedisConnection
@Autowired
private RedisConnectionFactory redisConnectionFactory;
public void test () {
// 创建消息记录, 以及指定stream
ByteRecord byteRecord = StreamRecords.rawBytes(Collections.singletonMap("name".getBytes(), "KevinBlandy".getBytes())).withStreamKey("mystream".getBytes());
// 获取连接
RedisConnection redisConnection = this.redisConnectionFactory.getConnection();
RecordId recordId = redisConnection.xAdd(byteRecord);
// 是否是自动生成的
boolean autoGenerated = recordId.shouldBeAutoGenerated();
// id值
String value = recordId.getValue();
// 序列号部分
long sequence = recordId.getSequence();
// 时间戳部分
long timestamp = recordId.getTimestamp();
}
4 往Stream推送对象消息
@Data
class Book {
private String title;
private String author;
public static Book create() {
Book book = new Book();
book.setTitle("xxx");
book.setAuthor("xxx");
return book;
}
}
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void sendRecord(String streamKey) {
Book book = Book.create();
log.info("产生一本书的信息:[{}]", book);
ObjectRecord<String, Book> record = StreamRecords.newRecord()
.in(streamKey)
.ofObject(book)
.withId(RecordId.autoGenerate());
RecordId recordId = redisTemplate.opsForStream()
.add(record);
log.info("返回的record-id:[{}]", recordId);
}
5 创建指定消费者组,如果之前存在这个消费者组,则删除
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private final AtomicBoolean isCreated = new AtomicBoolean(false);
@PostConstruct
public void groupInfo() {
// 发送个心跳,保证stream已经存在
HashMap<Object, Object> map = new HashMap<>();
map.put("fileBeat","fileBeat...");
final MapRecord<String, Object, Object> record = StreamRecords.newRecord()
.in(StreamConstant.Document.streamName)
.ofMap(map)
.withId(RecordId.autoGenerate());
final StreamOperations<String, Object, Object> stream = redisTemplate.opsForStream();
stream.add(record);
final StreamInfo.XInfoGroups xInfoGroups = stream.groups(StreamConstant.Document.streamName);
Collection<StreamInfo.XInfoGroup> needDestroyColl = new ArrayList<>();
xInfoGroups.forEach(xInfoStream -> {
if (xInfoStream.groupName().equals(StreamConstant.Document.consumerGroup)) {
isCreated.set(true);
} else {
needDestroyColl.add(xInfoStream);
}
});
for (StreamInfo.XInfoGroup xInfoGroup : needDestroyColl) {
log.info("destroy consumer group[{}]...", xInfoGroup.groupName());
stream.destroyGroup(StreamConstant.Document.streamName,xInfoGroup.groupName());
}
if (isCreated.get()) return;
log.info("create consumer group[{}]...", StreamConstant.Document.consumerGroup);
stream.createGroup(StreamConstant.Document.streamName, StreamConstant.Document.consumerGroup);
}
6 消费者组模式消费信息
6.1 阻塞消费
阻塞消费监听MapRecord
注意:
- MapRecord意思就是结果映射成一个
MapRecord<S, K, V>
,S是stream,k是自定义消息的key,V是具体消息,发送时也要发送MapRecord<S, K, V>
- 需要指定
.keySerializer(new StringRedisSerializer())
@Configuration
public class RedisStreamConfiguration {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private StateListener2 stateListener;
@Autowired
@Qualifier("stream-core-pool")
private ThreadPoolTaskExecutor executor;
@Bean(initMethod = "start", destroyMethod = "stop")
public StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer() {
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>>
options = StreamMessageListenerContainer.StreamMessageListenerContainerOptions
.builder()
// 一次最多获取多少条消息
.batchSize(5)
// 执行消息轮询的执行器
.executor(executor)
// 超时时间,设置为0,表示不超时(超时后会抛出异常)
.pollTimeout(Duration.ZERO)
// 消息消费异常的handler
.errorHandler(e-> log.error("发生了异常", e))
// 序列化器 或者RedisSerializer.string()
.serializer(new StringRedisSerializer())
.build();
StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer = StreamMessageListenerContainer
.create(this.redisConnectionFactory, options);
// 消费组B,手动ack
// receiveAutoAck(自动ack)
streamMessageListenerContainer.receive(
Consumer.from(StreamConstant.Document.consumerGroup, StreamConstant.Document.consumerName),
StreamOffset.create(StreamConstant.Document.streamName, ReadOffset.lastConsumed()),
stateListener
);
return streamMessageListenerContainer;
}
}
结合自己业务实现StreamListener
,并根据业务进行ack或删除消息
@Slf4j
@Configuration
public class StateListener implements StreamListener<String, MapRecord<String,String, String>> {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
@Async("stream-core-pool")
public void onMessage(MapRecord<String,String, String> message) {
StreamOperations<String, Object, Object> opsForStream = redisTemplate.opsForStream();
RecordId id = message.getId();
String value = message.getValue().get("xxx");
if (StrUtil.isBlank(value)){
log.error("接收到非法信息:{}",message.getValue());
opsForStream.acknowledge(StreamConstant.Document.streamName, StreamConstant.Document.consumerGroup, id);
opsForStream.delete(message);
return;
}
// 除去不合法字符
value = value.replaceAll("\"","");
log.info("received id:{} uuid:{}", id, value);
try {
// todo 业务
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
} finally {
try {
final Long acknowledge = opsForStream.acknowledge(
StreamConstant.Document.streamName,
StreamConstant.Document.consumerGroup,
id
);
Long delete = 0L;
if (acknowledge != null && acknowledge == 1L) {
delete = opsForStream.delete(message);
}
log.info("acknowledge:{} delete:{}",acknowledge, delete);
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
}
}
}
阻塞消费监听ObjectRecord
注意:
ObjectRecord
意思就是结果映射成一个对象,发送时也要发送对象- 需要指定
.keySerializer(new StringRedisSerializer())
- 使用
ObjectRecord
接收对象时,指定.objectMapper(new ObjectHashMapper())
@Configuration
public class RedisStreamConfiguration {
@Data
public static class Book {
private String title;
private String author;
public static Book create() {
Book book = new Book();
book.setTitle("xxx");
book.setAuthor("xxx");
return book;
}
}
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private StateListener stateListener;
@Autowired
@Qualifier("stream-core-pool")
private ThreadPoolTaskExecutor executor;
@Bean(initMethod = "start", destroyMethod = "stop")
public StreamMessageListenerContainer<String, ObjectRecord<String, Book>> streamMessageListenerContainer() {
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, Book>>
options = StreamMessageListenerContainer.StreamMessageListenerContainerOptions
.builder()
// 一次最多获取多少条消息
.batchSize(5)
// 执行消息轮询的执行器
.executor(executor)
// 超时时间,设置为0,表示不超时(超时后会抛出异常)
.pollTimeout(Duration.ZERO)
// 消息消费异常的handler
.errorHandler(new CustomErrorHandler())
// 序列化器 或者RedisSerializer.string()
.serializer(new StringRedisSerializer())
.keySerializer(new StringRedisSerializer())
.hashKeySerializer(new StringRedisSerializer())
.hashValueSerializer(new StringRedisSerializer())
.objectMapper(new ObjectHashMapper())
.targetType(Book.class)
.build();
StreamMessageListenerContainer<String, ObjectRecord<String, Book>> streamMessageListenerContainer =
StreamMessageListenerContainer.create(redisConnectionFactory, options);
// 消费组B,手动ack
// receiveAutoAck(自动ack)
streamMessageListenerContainer.receive(
Consumer.from(StreamConstant.Document.consumerGroup, StreamConstant.Document.consumerName),
StreamOffset.create(StreamConstant.Document.streamName, ReadOffset.lastConsumed()),
stateListener
);
return streamMessageListenerContainer;
}
}
结合自己业务实现StreamListener
,并根据业务进行ack或删除消息
@Slf4j
@Configuration
public class StateListener implements StreamListener<String, ObjectRecord<String, Book>> {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
@Async("stream-core-pool")
public void onMessage(ObjectRecord<String, Book> message) {
// 同上
}
}
6.2 非阻塞消费
主要是通过StreamOperations 或者是 RedicConnection 的消费API来进行消息的随机消费
从RedisTemplate中获取到StreamOperations
StreamOperations<String, Object, Object> opsForStream = redisTemplate.opsForStream();
StreamOperations 的读取 API:
// 随机范围读取
<V> List<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range)
<V> List<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range, Limit limit)
// 根据消息ID或者偏移量读取
List<MapRecord<K, HK, HV>> read(StreamOffset<K>... streams)
<V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams)
List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams)
<V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamReadOptions readOptions, StreamOffset<K>... streams)
List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamOffset<K>... streams)
<V> List<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamOffset<K>... streams)
List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams)
List<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams)
// 随机逆向范围读取
List<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range)
List<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit)
<V> List<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range)
<V> List<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range, Limit limit)
// 消费者信息
XInfoConsumers consumers(K key, String group);
// 消费者信息
XInfoGroups groups(K key);
// stream信息
XInfoStream info(K key);
// 获取消费组,消费者中未确认的消息
PendingMessagesSummary pending(K key, String group);
PendingMessages pending(K key, Consumer consumer)
PendingMessages pending(K key, String group, Range<?> range, long count)
PendingMessages pending(K key, String group, Range<?> range, long count)
7 监听未ACK的消息并处理
注意:使用此方式需要给redisTemplate的hashValue序列化方式配置为String
@Component
@Slf4j
public class NonAckHandle {
@Autowired
@Qualifier("myStringRedisTemplate")
private RedisTemplate<String, Object> redisTemplate;
// @Scheduled(cron = "0 0/1 * * * ?")
@Scheduled(cron = "0/10 0/1 * * * ?")
public void doMonitor2() {
// 一次取10条未ack的消息
StreamOperations<String, String, String> stream = redisTemplate.opsForStream();
// XPENDING doc-state-stream doc-state-stream-group - + 10
PendingMessages pendingMessages = stream.pending(
StreamConstant.Document.streamName,
StreamConstant.Document.consumerGroup,
Range.of(Range.Bound.inclusive("-"),Range.Bound.inclusive("+")),
10
);
// 如果空
if (pendingMessages.isEmpty()) {
return;
}
for (PendingMessage next : pendingMessages) {
Consumer consumer = next.getConsumer();
RecordId recordId = next.getId();
log.warn("consumer={} RecordId={}", consumer, recordId);
// XRANGE doc-state-stream 1676974558423 1676974558423 count 1
final List<MapRecord<String, String, String>> res = stream.range(
StreamConstant.Document.streamName,
Range.of(Range.Bound.inclusive(recordId.getValue()),Range.Bound.inclusive(recordId.getValue())),
RedisZSetCommands.Limit.limit().count(1)
);
//
// List<MapRecord<String, String, String>> res2 = stream.read(
// StreamReadOptions.empty().count(1),
// StreamOffset.create(StreamConstant.Document.streamName, ReadOffset.from(recordId))
// );
res.forEach(ele -> this.acknowledge(ele, stream, recordId));
}
}
public void acknowledge(MapRecord<String, String, String> message, StreamOperations<String, String, String> opsForStream, RecordId recordId) {
String value = message.getValue().get("fileUuid");
if (StrUtil.isBlank(value)){
log.warn("接收到非法信息:{}", message.getValue());
ackAndDel(message, opsForStream, recordId);
return;
}
// 除去不合法字符
value = value.replaceAll("\"","");
log.info("received id:{} uuid:{}", recordId, value);
try {
// todo 业务
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
} finally {
try {
ackAndDel(message, opsForStream, recordId);
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
}
}
private void ackAndDel(MapRecord<String, String, String> message, StreamOperations<String, String, String> opsForStream, RecordId recordId) {
final Long acknowledge = opsForStream.acknowledge(
StreamConstant.Document.streamName,
StreamConstant.Document.consumerGroup,
recordId
);
Long delete = 0L;
if (acknowledge != null && acknowledge == 1L) {
delete = opsForStream.delete(message);
}
log.info("acknowledge:{} delete:{}",acknowledge, delete);
}
}