最近在研究使用 自定义注解 + 拦截器 + Redis 实现限流 的功能时,需要用Redis记录一段时间内某个接口被请求的次数。发现一个问题:我使用的是RedisTemplate,当我在redis中插入一个myKey值,并且设置了对应过期时间. 当过期时间还没到的时候重新 更新 myKey值会导致 过期时间被刷新,不信邪的我直接在Redis-cli 中又试了一下,发现过期时间依然被刷新了。
下面是我的测试代码:
@Test
public void redisTest04() throws InterruptedException {
stringIntegerRedisTemplate.opsForValue().set("testData", 12, 20, TimeUnit.SECONDS);
log.info("testData value : [ {} ] 过期时间 --001 [ {} ]", stringIntegerRedisTemplate.opsForValue().get("testData"),
stringIntegerRedisTemplate.getExpire("testData"));
Thread.sleep(5000);
stringIntegerRedisTemplate.opsForValue().set("testData", 13);
log.info("testData value : [ {} ] 过期时间 --002 [ {} ]", stringIntegerRedisTemplate.opsForValue().get("testData"),
stringIntegerRedisTemplate.getExpire("testData"));
}
测试结果如下:
结果,在set的时候,这条数据变成了永久有效了,因为 redisTemplate.opsForValue().set()这个方法,如果不传过期时间的话,它就默认是永不过期的。
针对这个问题: 我查看了下redis的官方文档, 他们是这么解释的:
The timeout will only be cleared by commands that delete or overwrite the contents of the key, including DEL, SET, GETSET and all the *STORE commands. This means that all the operations that conceptually alter the value stored at the key without replacing it with a new one will leave the timeout untouched. For instance, incrementing the value of a key with INCR, pushing a new value into a list with LPUSH, or altering the field value of a hash with HSET are all operations that will leave the timeout untouched.
解释:
如果用DEL, SET, GETSET会将key对应存储的值替换成新的,命令也会清除掉超时时间;如果list结构中添加一个数据或者改变hset数据的一个字段是不会清除超时时间的;如果想要通过set去覆盖值那就必须重新设置expire。
这么看来,过期时间被刷新是无法避免的了,那么我的功能怎么实现呢?只能想办法了呀。
方案一
- 既然每次重新set的时候,就算我不设置过期时间它也会自动刷新,那么如果我在set之前拿到它的过期时间,在set新值的同时将获取到的过期时间也set进去,这样虽然有些误差,但是如果不是对时间要求特别严格,也是一种可取的办法。看我写的一个test:
@Resource private RedisTemplate<String, Integer> stringIntegerRedisTemplate; @Test public void redisTest01() throws InterruptedException { stringIntegerRedisTemplate.opsForValue().set("testData", 12, 20, TimeUnit.SECONDS); log.info("testData value : [ {} ] 过期时间 --001 [ {} ]", stringIntegerRedisTemplate.opsForValue().get("testData"), stringIntegerRedisTemplate.getExpire("testData")); Thread.sleep(5000); stringIntegerRedisTemplate.opsForValue() .set("testData", 13, stringIntegerRedisTemplate.getExpire("testData"), TimeUnit.SECONDS); log.info("testData value : [ {} ] 过期时间 --002 [ {} ]", stringIntegerRedisTemplate.opsForValue().get("testData"), stringIntegerRedisTemplate.getExpire("testData")); }
输出结果:
可见这种做法是行的通的。
但是,这其中肯定是存在误差的,如果时间要求严格的话,这样是不行的。那么我们就得另想办法了。
方案二
- 好在Redis存在一些方法解决我们的问题。我们使用的RedisTemplate有个 expireAt(K key, final Date date) 的方法,可以设置键到那个时间过期。那么只要我们将键的过期时间取出来,加上此时的时间,就能得到它会在哪个时间过期,在reset值后,用expireAt方法设置过期时间就OK了,看下面的demo:
@Test
public void test004() throws InterruptedException {
redisTemplate.opsForValue().set("time", 12, 30, TimeUnit.SECONDS);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DateUtil.DATE_FORMAT_SECOND);
Long time = redisTemplate.getExpire("time");
System.out.println(simpleDateFormat.format(new Date(Instant.now().toEpochMilli())) + ",过期时间001: " + time);
Date date = new Date(Instant.now().toEpochMilli() + time * 1000);
System.out.println("到 " + simpleDateFormat.format(date) + "过期");
Thread.sleep(5000);
redisTemplate.expireAt("time", date);
System.out.println(simpleDateFormat.format(new Date(Instant.now().toEpochMilli())) + ",过期时间002: " + redisTemplate.getExpire("time"));
}
输出结果如下:
2020-01-09 17:09:22,过期时间001: 30
到 2020-01-09 17:09:52过期
2020-01-09 17:09:27,过期时间002: 25
这样就没有误差了!