Commit 46f3790492492bfaee83cf33039a445de9787b6d

Authored by gaibu
1 parent a6c92816

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

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