Commit 361e50e5de3abd4551e05ba974ffc3c27d26c2c4
1 parent
d7656535
feat:Redis Stream 增加清理 Job,避免占用内存过多
Showing
3 changed files
with
87 additions
and
3 deletions
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
| @@ -6,6 +6,7 @@ import cn.hutool.system.SystemUtil; | @@ -6,6 +6,7 @@ import cn.hutool.system.SystemUtil; | ||
| 6 | import cn.iocoder.yudao.framework.common.enums.DocumentEnum; | 6 | import cn.iocoder.yudao.framework.common.enums.DocumentEnum; |
| 7 | import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; | 7 | import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; |
| 8 | import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob; | 8 | import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob; |
| 9 | +import cn.iocoder.yudao.framework.mq.redis.core.job.RedisStreamMessageCleanupJob; | ||
| 9 | import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; | 10 | import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; |
| 10 | import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; | 11 | import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; |
| 11 | import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; | 12 | import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; |
| @@ -74,6 +75,17 @@ public class YudaoRedisMQConsumerAutoConfiguration { | @@ -74,6 +75,17 @@ public class YudaoRedisMQConsumerAutoConfiguration { | ||
| 74 | } | 75 | } |
| 75 | 76 | ||
| 76 | /** | 77 | /** |
| 78 | + * 创建 Redis Stream 消息清理任务 | ||
| 79 | + */ | ||
| 80 | + @Bean | ||
| 81 | + @ConditionalOnBean(AbstractRedisStreamMessageListener.class) | ||
| 82 | + public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners, | ||
| 83 | + RedisMQTemplate redisTemplate, | ||
| 84 | + RedissonClient redissonClient) { | ||
| 85 | + return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient); | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + /** | ||
| 77 | * 创建 Redis Stream 集群消费的容器 | 89 | * 创建 Redis Stream 集群消费的容器 |
| 78 | * | 90 | * |
| 79 | * 基础知识:<a href="https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html">Redis Stream 的 xreadgroup 命令</a> | 91 | * 基础知识:<a href="https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html">Redis Stream 的 xreadgroup 命令</a> |
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java
| @@ -23,13 +23,13 @@ import java.util.Objects; | @@ -23,13 +23,13 @@ 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:pending:msg:lock"; | 26 | + private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock"; |
| 27 | 27 | ||
| 28 | /** | 28 | /** |
| 29 | * 消息超时时间,默认 5 分钟 | 29 | * 消息超时时间,默认 5 分钟 |
| 30 | * | 30 | * |
| 31 | * 1. 超时的消息才会被重新投递 | 31 | * 1. 超时的消息才会被重新投递 |
| 32 | - * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息5分钟过期后,再等 1 分钟才会被扫瞄到 | 32 | + * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息 5 分钟过期后,再等 1 分钟才会被扫瞄到 |
| 33 | */ | 33 | */ |
| 34 | private static final int EXPIRE_TIME = 5 * 60; | 34 | private static final int EXPIRE_TIME = 5 * 60; |
| 35 | 35 | ||
| @@ -39,7 +39,7 @@ public class RedisPendingMessageResendJob { | @@ -39,7 +39,7 @@ public class RedisPendingMessageResendJob { | ||
| 39 | private final RedissonClient redissonClient; | 39 | private final RedissonClient redissonClient; |
| 40 | 40 | ||
| 41 | /** | 41 | /** |
| 42 | - * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题 | 42 | + * 一分钟执行一次,这里选择每分钟的 35 秒执行,是为了避免整点任务过多的问题 |
| 43 | */ | 43 | */ |
| 44 | @Scheduled(cron = "35 * * * * ?") | 44 | @Scheduled(cron = "35 * * * * ?") |
| 45 | public void messageResend() { | 45 | public void messageResend() { |
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisStreamMessageCleanupJob.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.mq.redis.core.job; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; | ||
| 4 | +import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; | ||
| 5 | +import lombok.AllArgsConstructor; | ||
| 6 | +import lombok.extern.slf4j.Slf4j; | ||
| 7 | +import org.redisson.api.RLock; | ||
| 8 | +import org.redisson.api.RedissonClient; | ||
| 9 | +import org.springframework.data.redis.core.StreamOperations; | ||
| 10 | +import org.springframework.scheduling.annotation.Scheduled; | ||
| 11 | + | ||
| 12 | +import java.util.List; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * Redis Stream 消息清理任务 | ||
| 16 | + * 用于定期清理已消费的消息,防止内存占用过大 | ||
| 17 | + * | ||
| 18 | + * @see <a href="https://www.cnblogs.com/nanxiang/p/16179519.html">记一次 redis stream 数据类型内存不释放问题</a> | ||
| 19 | + * | ||
| 20 | + * @author 芋道源码 | ||
| 21 | + */ | ||
| 22 | +@Slf4j | ||
| 23 | +@AllArgsConstructor | ||
| 24 | +public class RedisStreamMessageCleanupJob { | ||
| 25 | + | ||
| 26 | + private static final String LOCK_KEY = "redis:stream:message-cleanup:lock"; | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 保留的消息数量,默认保留最近 10000 条消息 | ||
| 30 | + */ | ||
| 31 | + private static final long MAX_COUNT = 10000; | ||
| 32 | + | ||
| 33 | + private final List<AbstractRedisStreamMessageListener<?>> listeners; | ||
| 34 | + private final RedisMQTemplate redisTemplate; | ||
| 35 | + private final RedissonClient redissonClient; | ||
| 36 | + | ||
| 37 | + /** | ||
| 38 | + * 每小时执行一次清理任务 | ||
| 39 | + */ | ||
| 40 | + @Scheduled(cron = "0 0 * * * ?") | ||
| 41 | + public void cleanup() { | ||
| 42 | + RLock lock = redissonClient.getLock(LOCK_KEY); | ||
| 43 | + // 尝试加锁 | ||
| 44 | + if (lock.tryLock()) { | ||
| 45 | + try { | ||
| 46 | + execute(); | ||
| 47 | + } catch (Exception ex) { | ||
| 48 | + log.error("[cleanup][执行异常]", ex); | ||
| 49 | + } finally { | ||
| 50 | + lock.unlock(); | ||
| 51 | + } | ||
| 52 | + } | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + /** | ||
| 56 | + * 执行清理逻辑 | ||
| 57 | + */ | ||
| 58 | + private void execute() { | ||
| 59 | + StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream(); | ||
| 60 | + listeners.forEach(listener -> { | ||
| 61 | + try { | ||
| 62 | + // 使用 XTRIM 命令清理消息,只保留最近的 MAX_LEN 条消息 | ||
| 63 | + Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true); | ||
| 64 | + if (trimCount != null && trimCount > 0) { | ||
| 65 | + log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount); | ||
| 66 | + } | ||
| 67 | + } catch (Exception ex) { | ||
| 68 | + log.error("[execute][Stream({}) 清理异常]", listener.getStreamKey(), ex); | ||
| 69 | + } | ||
| 70 | + }); | ||
| 71 | + } | ||
| 72 | +} | ||
| 0 | \ No newline at end of file | 73 | \ No newline at end of file |