0.创建StreamMessageListenerContainer
作为管理消息队列的容器,它的功能有:
- 能轮询获取一个或多个stream的消息。
- 为stream绑定消费者信息(消费者组,消费者名字),注意:多个消费者组可以获取(订阅)一个stream的消息,但消费者组内的竞争获取消息,一个消费者组只有一个消费者能获取到消息。
- 自定义线程池,用来轮询获取消息队列的消息,再回调相应的StreamListener实现类(消费者)
1.创建StreamMessageListenerContainerOptions,用来配置消费队列容器。
2.创建ExecutorService
自定义线程池,用来轮询获取消息队列的消息,再回调相应的StreamListener实现类(消费者)
3.创建RedisConnectionFactory
不使用RedisTemplate,因为它用于简单的 Redis 操作,但并不提供类似消息监听或消费的高级功能。
@Configuration
@RequiredArgsConstructor
public class RedisStreamConfiguration {
private final RedisConnectionFactory redisConnectionFactory;
private final ShortLinkStatsSaveConsumer shortLinkStatsSaveConsumer;
@Value("${spring.data.redis.channel-topic.short-link-stats}")
private String topic;
@Value("${spring.data.redis.channel-topic.short-link-stats-group}")
private String group;
@Bean
public ExecutorService asyncStreamConsumer() {
AtomicInteger index = new AtomicInteger();
int processors = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(processors,
processors + processors >> 1,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
runnable -> {
Thread thread = new Thread(runnable);
thread.setName("stream_consumer_short-link_stats_" + index.incrementAndGet());
thread.setDaemon(true);
return thread;
}
);
}
@Bean(initMethod = "start", destroyMethod = "stop")
public StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer(ExecutorService asyncStreamConsumer) {
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =
StreamMessageListenerContainer.StreamMessageListenerContainerOptions
.builder()
// 一次最多获取多少条消息
.batchSize(10)
// 执行从 Stream 拉取到消息的任务流程
.executor(asyncStreamConsumer)
// 如果没有拉取到消息,需要阻塞的时间。不能大于 ${spring.data.redis.timeout},否则会超时
.pollTimeout(Duration.ofSeconds(3))
.build();
StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer =
StreamMessageListenerContainer.create(redisConnectionFactory, options);
streamMessageListenerContainer.receiveAutoAck(Consumer.from(group, "stats-consumer"),
StreamOffset.create(topic, ReadOffset.lastConsumed()), shortLinkStatsSaveConsumer);
return streamMessageListenerContainer;
}
}
4.创建消费者,实现StreamListener接口
@Slf4j
@Component
@RequiredArgsConstructor
public class ShortLinkStatsSaveConsumer implements StreamListener<String, MapRecord<String, String, String>> {
@Value("${short-link.stats.locale.amap-key}")
private String statsLocaleAmapKey;
@Override
public void onMessage(MapRecord<String, String, String> message) {
String stream = message.getStream();
RecordId id = message.getId();
Map<String, String> producerMap = message.getValue();
String fullShortUrl = producerMap.get("fullShortUrl");
if (StrUtil.isNotBlank(fullShortUrl)) {
String gid = producerMap.get("gid");
ShortLinkStatsRecordDTO statsRecord = JSON.parseObject(producerMap.get("statsRecord"), ShortLinkStatsRecordDTO.class);
actualSaveShortLinkStats(fullShortUrl, gid, statsRecord);
}
stringRedisTemplate.opsForStream().delete(Objects.requireNonNull(stream), id.getValue());
}
5.创建生产者
@Component
@RequiredArgsConstructor
public class ShortLinkStatsSaveProducer {
private final StringRedisTemplate stringRedisTemplate;
@Value("${spring.data.redis.channel-topic.short-link-stats}")
private String topic;
/**
* 发送延迟消费短链接统计
*/
public void send(Map<String, String> producerMap) {
stringRedisTemplate.opsForStream().add(topic, producerMap);
}
}