Commit f469c5caaf7304aec07b6ae3285fdf0eaacb6790

Authored by 芋道源码
Committed by Gitee
2 parents 75b3a297 604266d3

!332 redis mq消息丢失问题

Merge pull request !332 from 与或非/issues/I5RFO2
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java
... ... @@ -8,8 +8,11 @@ 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.job.RedisPendingMessageResendJob;
11 12 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
12 13 import lombok.extern.slf4j.Slf4j;
  14 +import org.redisson.api.RedissonClient;
  15 +import org.springframework.beans.factory.annotation.Value;
13 16 import org.springframework.boot.autoconfigure.AutoConfiguration;
14 17 import org.springframework.context.annotation.Bean;
15 18 import org.springframework.data.redis.connection.RedisServerCommands;
... ... @@ -24,7 +27,7 @@ import org.springframework.data.redis.listener.ChannelTopic;
24 27 import org.springframework.data.redis.listener.RedisMessageListenerContainer;
25 28 import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainerX;
26 29 import org.springframework.data.redis.stream.StreamMessageListenerContainer;
27   -import org.springframework.scheduling.annotation.Async;
  30 +import org.springframework.scheduling.annotation.EnableScheduling;
28 31  
29 32 import java.util.List;
30 33 import java.util.Properties;
... ... @@ -35,6 +38,7 @@ import java.util.Properties;
35 38 * @author 芋道源码
36 39 */
37 40 @Slf4j
  41 +@EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息
38 42 @AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
39 43 public class YudaoMQAutoConfiguration {
40 44  
... ... @@ -70,8 +74,19 @@ public class YudaoMQAutoConfiguration {
70 74 }
71 75  
72 76 /**
  77 + * 创建 Redis Stream 重新消费的任务
  78 + */
  79 + @Bean
  80 + public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractStreamMessageListener<?>> listeners,
  81 + RedisMQTemplate redisTemplate,
  82 + @Value("${spring.application.name}") String groupName,
  83 + RedissonClient redissonClient) {
  84 + return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient);
  85 + }
  86 +
  87 + /**
73 88 * 创建 Redis Stream 集群消费的容器
74   - *
  89 + * <p>
75 90 * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html
76 91 */
77 92 @Bean(initMethod = "start", destroyMethod = "stop")
... ... @@ -99,7 +114,8 @@ public class YudaoMQAutoConfiguration {
99 114 // 创建 listener 对应的消费者分组
100 115 try {
101 116 redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup());
102   - } catch (Exception ignore) {}
  117 + } catch (Exception ignore) {
  118 + }
103 119 // 设置 listener 对应的 redisTemplate
104 120 listener.setRedisMQTemplate(redisMQTemplate);
105 121 // 创建 Consumer 对象
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/job/RedisPendingMessageResendJob.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mq.job;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
  5 +import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener;
  6 +import lombok.AllArgsConstructor;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.redisson.api.RLock;
  9 +import org.redisson.api.RedissonClient;
  10 +import org.springframework.data.redis.connection.stream.Consumer;
  11 +import org.springframework.data.redis.connection.stream.MapRecord;
  12 +import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
  13 +import org.springframework.data.redis.connection.stream.ReadOffset;
  14 +import org.springframework.data.redis.connection.stream.StreamOffset;
  15 +import org.springframework.data.redis.connection.stream.StreamRecords;
  16 +import org.springframework.data.redis.core.StreamOperations;
  17 +import org.springframework.scheduling.annotation.Scheduled;
  18 +
  19 +import java.util.List;
  20 +import java.util.Map;
  21 +
  22 +/**
  23 + * 这个任务用于处理,crash 之后的消费者未消费完的消息
  24 + */
  25 +@Slf4j
  26 +@AllArgsConstructor
  27 +public class RedisPendingMessageResendJob {
  28 +
  29 + private static final String LOCK_KEY = "redis:pending:msg:lock";
  30 +
  31 + private final List<AbstractStreamMessageListener<?>> listeners;
  32 + private final RedisMQTemplate redisTemplate;
  33 + private final String groupName;
  34 + private final RedissonClient redissonClient;
  35 +
  36 + /**
  37 + * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题
  38 + */
  39 + @Scheduled(cron = "35 * * * * ?")
  40 + public void messageResend() {
  41 + RLock lock = redissonClient.getLock(LOCK_KEY);
  42 + // 尝试加锁
  43 + if (lock.tryLock()) {
  44 + try {
  45 + execute();
  46 + } catch (Exception ex) {
  47 + log.error("[messageResend][执行异常]", ex);
  48 + } finally {
  49 + lock.unlock();
  50 + }
  51 + }
  52 + }
  53 +
  54 + private void execute() {
  55 + StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
  56 + listeners.forEach(listener -> {
  57 + PendingMessagesSummary pendingMessagesSummary = ops.pending(listener.getStreamKey(), groupName);
  58 + // 每个消费者的 pending 队列消息数量
  59 + Map<String, Long> pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer();
  60 + pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> {
  61 + log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount);
  62 +
  63 + // 从消费者的 pending 队列中读取消息
  64 + List<MapRecord<String, Object, Object>> records = ops.read(Consumer.from(groupName, consumerName), StreamOffset.create(listener.getStreamKey(), ReadOffset.from("0")));
  65 + if (CollUtil.isEmpty(records)) {
  66 + return;
  67 + }
  68 + for (MapRecord<String, Object, Object> record : records) {
  69 + // 重新投递消息
  70 + redisTemplate.getRedisTemplate().opsForStream().add(StreamRecords.newRecord()
  71 + .ofObject(record.getValue()) // 设置内容
  72 + .withStreamKey(listener.getStreamKey()));
  73 +
  74 + // ack 消息消费完成
  75 + redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, record);
  76 + }
  77 + });
  78 + });
  79 + }
  80 +}
... ...