Commit 0f31ce7ecf0b872f8cf8cc83df66886fa8488140

Authored by gaibu
1 parent afa3e6dc

fix: 解决 redis mq 消息丢失问题

yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java
... ... @@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
8 8 import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
9 9 import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
10 10 import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener;
  11 +import cn.iocoder.yudao.framework.mq.scheduler.PendingMessageScheduler;
11 12 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
12 13 import lombok.extern.slf4j.Slf4j;
13 14 import org.springframework.boot.autoconfigure.AutoConfiguration;
... ... @@ -24,7 +25,6 @@ import org.springframework.data.redis.listener.ChannelTopic;
24 25 import org.springframework.data.redis.listener.RedisMessageListenerContainer;
25 26 import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainerX;
26 27 import org.springframework.data.redis.stream.StreamMessageListenerContainer;
27   -import org.springframework.scheduling.annotation.Async;
28 28  
29 29 import java.util.List;
30 30 import java.util.Properties;
... ... @@ -70,6 +70,15 @@ public class YudaoMQAutoConfiguration {
70 70 }
71 71  
72 72 /**
  73 + *
  74 + * @return
  75 + */
  76 + @Bean
  77 + public PendingMessageScheduler pendingMessageScheduler(){
  78 + return new PendingMessageScheduler();
  79 + }
  80 +
  81 + /**
73 82 * 创建 Redis Stream 集群消费的容器
74 83 *
75 84 * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/scheduler/PendingMessageScheduler.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mq.scheduler;
  2 +
  3 +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
  4 +import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.springframework.beans.factory.annotation.Autowired;
  7 +import org.springframework.beans.factory.annotation.Value;
  8 +import org.springframework.data.redis.connection.stream.Consumer;
  9 +import org.springframework.data.redis.connection.stream.MapRecord;
  10 +import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
  11 +import org.springframework.data.redis.connection.stream.ReadOffset;
  12 +import org.springframework.data.redis.connection.stream.StreamOffset;
  13 +import org.springframework.data.redis.connection.stream.StreamRecords;
  14 +import org.springframework.data.redis.core.StreamOperations;
  15 +import org.springframework.scheduling.annotation.EnableScheduling;
  16 +import org.springframework.scheduling.annotation.Scheduled;
  17 +
  18 +import java.util.List;
  19 +import java.util.Map;
  20 +
  21 +/**
  22 + * 这个定时器用于处理,crash 之后的消费者未消费完的消息
  23 + */
  24 +@Slf4j
  25 +@EnableScheduling
  26 +public class PendingMessageScheduler {
  27 +
  28 + @Autowired
  29 + private List<AbstractStreamMessageListener<?>> listeners;
  30 + @Autowired
  31 + private RedisMQTemplate redisTemplate;
  32 + @Value("${spring.application.name}")
  33 + private String groupName;
  34 +
  35 + /**
  36 + * 一分钟执行一次
  37 + */
  38 + @Scheduled(fixedRate = 60 * 1000)
  39 + public void processPendingMessage() {
  40 + StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
  41 +
  42 + for (AbstractStreamMessageListener<?> listener : listeners) {
  43 + PendingMessagesSummary pendingMessagesSummary = ops.pending(listener.getStreamKey(), groupName);
  44 + // 每个消费者的pending消息数量
  45 + Map<String, Long> pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer();
  46 + pendingMessagesPerConsumer.entrySet().forEach(entry -> {
  47 + String consumerName = entry.getKey();
  48 + Long pendingMessageCount = entry.getValue();
  49 + log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount);
  50 +
  51 + // 从消费者的pending队列中读取消息
  52 + List<MapRecord<String, Object, Object>> retVal = ops.read(Consumer.from(groupName, consumerName), StreamOffset.create(listener.getStreamKey(), ReadOffset.from("0")));
  53 +
  54 + for (MapRecord<String, Object, Object> record : retVal) {
  55 + // 重新投递消息
  56 + redisTemplate.getRedisTemplate().opsForStream().add(StreamRecords.newRecord()
  57 + .ofObject(record.getValue()) // 设置内容
  58 + .withStreamKey(listener.getStreamKey()));
  59 +
  60 + // ack 消息消费完成
  61 + redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, record);
  62 + }
  63 + });
  64 + }
  65 + }
  66 +}
... ...