Commit 5e8df3089d35a04f61bc05ebddd5f5b17d0ac2be

Authored by panda
1 parent 3f1d86ef

fix: 修复IoT使用redis消息队列时iot_device_message的STREAM的消息无限积压问题

yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
@@ -12,7 +12,6 @@ import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessag @@ -12,7 +12,6 @@ import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessag
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; 14 import org.redisson.api.RedissonClient;
15 -import org.springframework.beans.factory.annotation.Value;  
16 import org.springframework.boot.autoconfigure.AutoConfiguration; 15 import org.springframework.boot.autoconfigure.AutoConfiguration;
17 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 16 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
18 import org.springframework.context.annotation.Bean; 17 import org.springframework.context.annotation.Bean;
@@ -70,7 +69,8 @@ public class YudaoRedisMQConsumerAutoConfiguration { @@ -70,7 +69,8 @@ public class YudaoRedisMQConsumerAutoConfiguration {
70 public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners, 69 public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
71 RedisMQTemplate redisTemplate, 70 RedisMQTemplate redisTemplate,
72 RedissonClient redissonClient) { 71 RedissonClient redissonClient) {
73 - return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient); 72 + return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient,
  73 + RedisPendingMessageResendJob.DEFAULT_RESEND_LOCK_KEY);
74 } 74 }
75 75
76 /** 76 /**
@@ -81,7 +81,8 @@ public class YudaoRedisMQConsumerAutoConfiguration { @@ -81,7 +81,8 @@ public class YudaoRedisMQConsumerAutoConfiguration {
81 public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners, 81 public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners,
82 RedisMQTemplate redisTemplate, 82 RedisMQTemplate redisTemplate,
83 RedissonClient redissonClient) { 83 RedissonClient redissonClient) {
84 - return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient); 84 + return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient,
  85 + RedisStreamMessageCleanupJob.DEFAULT_CLEANUP_LOCK_KEY);
85 } 86 }
86 87
87 /** 88 /**
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java
@@ -23,7 +23,9 @@ import java.util.Objects; @@ -23,7 +23,9 @@ import java.util.Objects;
23 @AllArgsConstructor 23 @AllArgsConstructor
24 public class RedisPendingMessageResendJob { 24 public class RedisPendingMessageResendJob {
25 25
26 - private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock"; 26 + public static final String DEFAULT_RESEND_LOCK_KEY = "redis:stream:pending-message-resend:lock";
  27 +
  28 + public static final String IOT_RESEND_LOCK_KEY = "redis:stream:pending-message-resend:lock:iot";
27 29
28 /** 30 /**
29 * 消息超时时间,默认 5 分钟 31 * 消息超时时间,默认 5 分钟
@@ -36,22 +38,26 @@ public class RedisPendingMessageResendJob { @@ -36,22 +38,26 @@ public class RedisPendingMessageResendJob {
36 private final List<AbstractRedisStreamMessageListener<?>> listeners; 38 private final List<AbstractRedisStreamMessageListener<?>> listeners;
37 private final RedisMQTemplate redisTemplate; 39 private final RedisMQTemplate redisTemplate;
38 private final RedissonClient redissonClient; 40 private final RedissonClient redissonClient;
  41 + private final String resendLockKey;
39 42
40 /** 43 /**
41 * 一分钟执行一次,这里选择每分钟的 35 秒执行,是为了避免整点任务过多的问题 44 * 一分钟执行一次,这里选择每分钟的 35 秒执行,是为了避免整点任务过多的问题
42 */ 45 */
43 @Scheduled(cron = "35 * * * * ?") 46 @Scheduled(cron = "35 * * * * ?")
44 public void messageResend() { 47 public void messageResend() {
45 - RLock lock = redissonClient.getLock(LOCK_KEY);  
46 - // 尝试加锁 48 + RLock lock = redissonClient.getLock(resendLockKey);
47 if (lock.tryLock()) { 49 if (lock.tryLock()) {
48 try { 50 try {
49 execute(); 51 execute();
50 } catch (Exception ex) { 52 } catch (Exception ex) {
51 - log.error("[messageResend][执行异常]", ex); 53 + log.error("[messageResend][执行异常][lockKey={}]", resendLockKey, ex);
52 } finally { 54 } finally {
53 - lock.unlock(); 55 + if (lock.isHeldByCurrentThread()) {
  56 + lock.unlock();
  57 + }
54 } 58 }
  59 + } else {
  60 + log.debug("[messageResend][未获取到锁,跳过本轮][lockKey={}]", resendLockKey);
55 } 61 }
56 } 62 }
57 63
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisStreamMessageCleanupJob.java
@@ -23,7 +23,16 @@ import java.util.List; @@ -23,7 +23,16 @@ import java.util.List;
23 @AllArgsConstructor 23 @AllArgsConstructor
24 public class RedisStreamMessageCleanupJob { 24 public class RedisStreamMessageCleanupJob {
25 25
26 - private static final String LOCK_KEY = "redis:stream:message-cleanup:lock"; 26 + /**
  27 + * 业务 MQ(Spring 容器内 AbstractRedisStreamMessageListener)清理任务使用的分布式锁
  28 + */
  29 + public static final String DEFAULT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock";
  30 +
  31 + /**
  32 + * IoT Redis 总线清理任务使用的分布式锁(须与 {@link #DEFAULT_CLEANUP_LOCK_KEY} 区分,否则会共抢一把锁,
  33 + * 同一时刻只有一侧能执行 XTRIM,另一侧 Stream 可能无限积压)
  34 + */
  35 + public static final String IOT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock:iot";
27 36
28 /** 37 /**
29 * 保留的消息数量,默认保留最近 10000 条消息 38 * 保留的消息数量,默认保留最近 10000 条消息
@@ -33,22 +42,29 @@ public class RedisStreamMessageCleanupJob { @@ -33,22 +42,29 @@ public class RedisStreamMessageCleanupJob {
33 private final List<AbstractRedisStreamMessageListener<?>> listeners; 42 private final List<AbstractRedisStreamMessageListener<?>> listeners;
34 private final RedisMQTemplate redisTemplate; 43 private final RedisMQTemplate redisTemplate;
35 private final RedissonClient redissonClient; 44 private final RedissonClient redissonClient;
  45 + /**
  46 + * Redisson 锁键(多 Bean 注册清理任务时必须各不相同)
  47 + */
  48 + private final String cleanupLockKey;
36 49
37 /** 50 /**
38 * 每小时执行一次清理任务 51 * 每小时执行一次清理任务
39 */ 52 */
40 @Scheduled(cron = "0 0 * * * ?") 53 @Scheduled(cron = "0 0 * * * ?")
41 public void cleanup() { 54 public void cleanup() {
42 - RLock lock = redissonClient.getLock(LOCK_KEY);  
43 - // 尝试加锁 55 + RLock lock = redissonClient.getLock(cleanupLockKey);
44 if (lock.tryLock()) { 56 if (lock.tryLock()) {
45 try { 57 try {
46 execute(); 58 execute();
47 } catch (Exception ex) { 59 } catch (Exception ex) {
48 - log.error("[cleanup][执行异常]", ex); 60 + log.error("[cleanup][执行异常][lockKey={}]", cleanupLockKey, ex);
49 } finally { 61 } finally {
50 - lock.unlock(); 62 + if (lock.isHeldByCurrentThread()) {
  63 + lock.unlock();
  64 + }
51 } 65 }
  66 + } else {
  67 + log.debug("[cleanup][未获取到锁,跳过本轮][lockKey={}]", cleanupLockKey);
52 } 68 }
53 } 69 }
54 70
@@ -59,8 +75,8 @@ public class RedisStreamMessageCleanupJob { @@ -59,8 +75,8 @@ public class RedisStreamMessageCleanupJob {
59 StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream(); 75 StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
60 listeners.forEach(listener -> { 76 listeners.forEach(listener -> {
61 try { 77 try {
62 - // 使用 XTRIM 命令清理消息,只保留最近的 MAX_LEN 条消息  
63 - Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true); 78 + // 使用 XTRIM MAXLEN 精确裁剪(approximate=false),避免 ~ 模式下长期明显高于上限
  79 + Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, false);
64 if (trimCount != null && trimCount > 0) { 80 if (trimCount != null && trimCount > 0) {
65 log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount); 81 log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount);
66 } 82 }
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusAutoConfiguration.java
@@ -99,7 +99,8 @@ public class IotMessageBusAutoConfiguration { @@ -99,7 +99,8 @@ public class IotMessageBusAutoConfiguration {
99 RedisMQTemplate redisTemplate, 99 RedisMQTemplate redisTemplate,
100 RedissonClient redissonClient) { 100 RedissonClient redissonClient) {
101 List<AbstractRedisStreamMessageListener<?>> listeners = getListeners(messageBus); 101 List<AbstractRedisStreamMessageListener<?>> listeners = getListeners(messageBus);
102 - return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient); 102 + return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient,
  103 + RedisPendingMessageResendJob.IOT_RESEND_LOCK_KEY);
103 } 104 }
104 105
105 /** 106 /**
@@ -110,7 +111,8 @@ public class IotMessageBusAutoConfiguration { @@ -110,7 +111,8 @@ public class IotMessageBusAutoConfiguration {
110 RedisMQTemplate redisTemplate, 111 RedisMQTemplate redisTemplate,
111 RedissonClient redissonClient) { 112 RedissonClient redissonClient) {
112 List<AbstractRedisStreamMessageListener<?>> listeners = getListeners(messageBus); 113 List<AbstractRedisStreamMessageListener<?>> listeners = getListeners(messageBus);
113 - return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient); 114 + return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient,
  115 + RedisStreamMessageCleanupJob.IOT_CLEANUP_LOCK_KEY);
114 } 116 }
115 117
116 private List<AbstractRedisStreamMessageListener<?>> getListeners(IotRedisMessageBus messageBus) { 118 private List<AbstractRedisStreamMessageListener<?>> getListeners(IotRedisMessageBus messageBus) {