查看原文
其他

请勿过度依赖Redis的过期监听?

架构师 2023-06-18
架构师(JiaGouX)我们都是架构师!
架构未来,你来不来?


因公众号更改推送规则,请点“在看”并加“星标”第一时间获取精彩技术分享


Redis过期监听场景


业务中有类似等待一定时间之后执行某种行为的需求 , 比如30分钟之后关闭订单 . 网上有很多使用Redis过期监听的Demo , 但是其实这是个大坑 , 因为Redis不能确保key在指定时间被删除 , 也就造成了通知的延期 . 不多说 , 跑个测试


测试情况


先说环境 , redis 运行在Docker容器中 ,分配了 一个cpu以及512MB内存, 在Docker中执行 redis-benchmark -t set -r 100000 -n 1000000 结果如下:
====== SET ====== 1000000 requests completed in 171.03 seconds 50 parallel clients 3 bytes payload keep alive: 1 host configuration "save": 3600 1 300 100 60 10000 host configuration "appendonly": no multi-thread: no

其实这里有些不严谨 benchmark 线程不应该在Docker容器内部运行 . 跑分的时候大概 benchmark 和redis 主线程各自持有50%CPU

测试代码如下:

@Service@Slf4jpublic class RedisJob { @Autowired private StringRedisTemplate stringRedisTemplate; public DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public LocalDateTime end = LocalDateTime.of(LocalDate.of(2020, 5, 12), LocalTime.of(8, 0)); @Scheduled(cron = "0 56 * * * ?") public void initKeys() { LocalDateTime now = LocalDateTime.now(); ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); log.info("开始设置key"); LocalDateTime begin = now.withMinute(0).withSecond(0).withNano(0); for (int i = 1; i < 17; i++) { setExpireKey(begin.plusHours(i), 8, operations); } log.info("设置完毕: " + Duration.between(now, LocalDateTime.now())); } private void setExpireKey(LocalDateTime expireTime, int step, ValueOperations<String, String> operations) { LocalDateTime localDateTime = LocalDateTime.now().withNano(0); String nowTime = dateTimeFormatter.format(localDateTime); while (expireTime.getMinute() < 55) { operations.set(nowTime + "@" + dateTimeFormatter.format(expireTime), "A", Duration.between(expireTime, LocalDateTime.now()).abs()); expireTime = expireTime.plusSeconds(step); } }}

大概意思就是每小时56分的时候 , 会增加一批在接下来16小时过期的key , 过期时间间隔8秒 , 且过期时间都在55分之前

@Slf4j@Componentpublic class RedisKeyExpirationListener extends KeyExpirationEventMessageListener { public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } public DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Autowired private StringRedisTemplate stringRedisTemplate; @Override public void onMessage(Message message, byte[] pattern) { String keyName = new String(message.getBody()); LocalDateTime parse = LocalDateTime.parse(keyName.split("@")[1], dateTimeFormatter); long seconds = Duration.between(parse, LocalDateTime.now()).getSeconds(); stringRedisTemplate.execute((RedisCallback<Object>) connection -> { Long size = connection.dbSize(); log.info("过期key:" + keyName + " ,当前size:" + size + " ,滞后时间" + seconds); return null; }); }}

这里是监测到过期之后打印当前的dbSize 以及滞后时间


@Beanpublic RedisMessageListenerContainer configRedisMessageListenerContainer(RedisConnectionFactory connectionFactory) { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(100); executor.setMaxPoolSize(100); executor.setQueueCapacity(100); executor.setKeepAliveSeconds(3600); executor.setThreadNamePrefix("redis"); // rejection-policy:当pool已经达到max size的时候,如何处理新任务 // CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); RedisMessageListenerContainer container = new RedisMessageListenerContainer(); // 设置Redis的连接工厂 container.setConnectionFactory(connectionFactory); // 设置监听使用的线程池 container.setTaskExecutor(executor); // 设置监听的Topic return container;}

设置Redis的过期监听 以及线程池信息 , 

最后的测试结果是当key数量小于1万的时候 , 基本上都可以在10s内完成过期通知 , 但是如果数量到3万 , 就有部分key会延迟120s . 顺便贴一下我最新的日志

2020-05-13 22:16:48.383  : 过期key:2020-05-13 11:56:02@2020-05-13 22:14:08 ,当前size:57405 ,滞后时间1602020-05-13 22:16:49.389 : 过期key:2020-05-13 11:56:02@2020-05-13 22:14:32 ,当前size:57404 ,滞后时间1372020-05-13 22:16:49.591 : 过期key:2020-05-13 10:56:02@2020-05-13 22:13:20 ,当前size:57403 ,滞后时间2092020-05-13 22:16:50.093 : 过期key:2020-05-13 20:56:00@2020-05-13 22:12:32 ,当前size:57402 ,滞后时间2582020-05-13 22:16:50.596 : 过期key:2020-05-13 07:56:03@2020-05-13 22:13:28 ,当前size:57401 ,滞后时间2022020-05-13 22:16:50.697 : 过期key:2020-05-13 20:56:00@2020-05-13 22:14:32 ,当前size:57400 ,滞后时间1382020-05-13 22:16:50.999 : 过期key:2020-05-13 19:56:00@2020-05-13 22:13:44 ,当前size:57399 ,滞后时间1862020-05-13 22:16:51.199 : 过期key:2020-05-13 20:56:00@2020-05-13 22:14:40 ,当前size:57398 ,滞后时间1312020-05-13 22:16:52.205 : 过期key:2020-05-13 15:56:01@2020-05-13 22:16:24 ,当前size:57397 ,滞后时间282020-05-13 22:16:52.808 : 过期key:2020-05-13 06:56:03@2020-05-13 22:15:04 ,当前size:57396 ,滞后时间1082020-05-13 22:16:53.009 : 过期key:2020-05-13 06:56:03@2020-05-13 22:16:40 ,当前size:57395 ,滞后时间132020-05-13 22:16:53.110 : 过期key:2020-05-13 20:56:00@2020-05-13 22:14:56 ,当前size:57394 ,滞后时间1172020-05-13 22:16:53.211 : 过期key:2020-05-13 06:56:03@2020-05-13 22:13:44 ,当前size:57393 ,滞后时间1892020-05-13 22:16:53.613 : 过期key:2020-05-13 15:56:01@2020-05-13 22:12:24 ,当前size:57392 ,滞后时间2692020-05-13 22:16:54.317 : 过期key:2020-05-13 15:56:01@2020-05-13 22:16:00 ,当前size:57391 ,滞后时间542020-05-13 22:16:54.517 : 过期key:2020-05-13 18:56:00@2020-05-13 22:15:44 ,当前size:57390 ,滞后时间702020-05-13 22:16:54.618 : 过期key:2020-05-13 21:56:00@2020-05-13 22:14:24 ,当前size:57389 ,滞后时间1502020-05-13 22:16:54.819 : 过期key:2020-05-13 17:56:00@2020-05-13 22:14:40 ,当前size:57388 ,滞后时间1342020-05-13 22:16:55.322 : 过期key:2020-05-13 10:56:02@2020-05-13 22:13:52 ,当前size:57387 ,滞后时间1832020-05-13 22:16:55.423  : 过期key:2020-05-13 07:56:03@2020-05-13 22:14:16 ,当前size:57386 ,滞后时间159

可以看到 ,当数量到达5万的时候 , 大部分都已经滞后了两分钟 , 对于业务方来说已经完全无法忍受了


总结


可能到这里 , 你会说Redis 给你挖了一个大坑 , 但其实这些都在文档上写的明明白白
  • How Redis expires keys (https://redis.io/commands/expire#how-redis-expires-keys)
  • Timing of expired events (https://redis.io/topics/notifications#timing-of-expired-events)
尤其是在 Timing of expired events  中 , 明确的说明了 "Basically expired events are generated when the Redis server deletes the key and not when the time to live theoretically reaches the value of zero."  , 这两个文章读下来你会感觉 ,  卧槽Redis的过期策略其实也挺'Low'的
其实公众号看多了 , 你会发现大部分Demo都是互相抄来抄去 , 以及翻译官方Demo . 建议大家还是谨慎一些 , 真要使用的话 , 最好读一下官方文档 , 哪怕用百度翻译也要有一些自己的理解 .
文章比较枯燥 , 感谢大家耐心阅读 ,  如有建议 恳请留言讨论。


如喜欢本文,请点击右上角,把文章分享到朋友圈
如有想了解学习的技术点,请留言给若飞安排分享

·END·

相关阅读:


作者:迪壳

来源:juejin.cn/post/6844904158227595271


版权申明:内容来源网络,仅供分享学习,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

架构师

我们都是架构师!



关注架构师(JiaGouX),添加“星标”

获取每天技术干货,一起成为牛逼架构师

技术群请加若飞:1321113940 进架构师群

投稿、合作、版权等邮箱:admin@137x.com

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存