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 12 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
13 13 import lombok.extern.slf4j.Slf4j;
14 14 import org.redisson.api.RedissonClient;
15   -import org.springframework.beans.factory.annotation.Value;
16 15 import org.springframework.boot.autoconfigure.AutoConfiguration;
17 16 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
18 17 import org.springframework.context.annotation.Bean;
... ... @@ -70,7 +69,8 @@ public class YudaoRedisMQConsumerAutoConfiguration {
70 69 public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
71 70 RedisMQTemplate redisTemplate,
72 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 81 public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners,
82 82 RedisMQTemplate redisTemplate,
83 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 23 @AllArgsConstructor
24 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 31 * 消息超时时间,默认 5 分钟
... ... @@ -36,22 +38,26 @@ public class RedisPendingMessageResendJob {
36 38 private final List<AbstractRedisStreamMessageListener<?>> listeners;
37 39 private final RedisMQTemplate redisTemplate;
38 40 private final RedissonClient redissonClient;
  41 + private final String resendLockKey;
39 42  
40 43 /**
41 44 * 一分钟执行一次,这里选择每分钟的 35 秒执行,是为了避免整点任务过多的问题
42 45 */
43 46 @Scheduled(cron = "35 * * * * ?")
44 47 public void messageResend() {
45   - RLock lock = redissonClient.getLock(LOCK_KEY);
46   - // 尝试加锁
  48 + RLock lock = redissonClient.getLock(resendLockKey);
47 49 if (lock.tryLock()) {
48 50 try {
49 51 execute();
50 52 } catch (Exception ex) {
51   - log.error("[messageResend][执行异常]", ex);
  53 + log.error("[messageResend][执行异常][lockKey={}]", resendLockKey, ex);
52 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 23 @AllArgsConstructor
24 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 38 * 保留的消息数量,默认保留最近 10000 条消息
... ... @@ -33,22 +42,29 @@ public class RedisStreamMessageCleanupJob {
33 42 private final List<AbstractRedisStreamMessageListener<?>> listeners;
34 43 private final RedisMQTemplate redisTemplate;
35 44 private final RedissonClient redissonClient;
  45 + /**
  46 + * Redisson 锁键(多 Bean 注册清理任务时必须各不相同)
  47 + */
  48 + private final String cleanupLockKey;
36 49  
37 50 /**
38 51 * 每小时执行一次清理任务
39 52 */
40 53 @Scheduled(cron = "0 0 * * * ?")
41 54 public void cleanup() {
42   - RLock lock = redissonClient.getLock(LOCK_KEY);
43   - // 尝试加锁
  55 + RLock lock = redissonClient.getLock(cleanupLockKey);
44 56 if (lock.tryLock()) {
45 57 try {
46 58 execute();
47 59 } catch (Exception ex) {
48   - log.error("[cleanup][执行异常]", ex);
  60 + log.error("[cleanup][执行异常][lockKey={}]", cleanupLockKey, ex);
49 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 75 StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
60 76 listeners.forEach(listener -> {
61 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 80 if (trimCount != null && trimCount > 0) {
65 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 99 RedisMQTemplate redisTemplate,
100 100 RedissonClient redissonClient) {
101 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 111 RedisMQTemplate redisTemplate,
111 112 RedissonClient redissonClient) {
112 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 118 private List<AbstractRedisStreamMessageListener<?>> getListeners(IotRedisMessageBus messageBus) {
... ...