Commit 5e8df3089d35a04f61bc05ebddd5f5b17d0ac2be
1 parent
3f1d86ef
fix: 修复IoT使用redis消息队列时iot_device_message的STREAM的消息无限积压问题
Showing
4 changed files
with
42 additions
and
17 deletions
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) { | ... | ... |