请勿过度依赖Redis的过期监听
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 @Slf4j public 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 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 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 @Component public 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