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,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) { |