
作者 | 杨遥
随着我们项目的迭代升级,可能在发新版本的时候需要批量删除redis中的缓存数据,redis不像mysql一样,直接查询就能删除。这时候你可能首先想到的是keys *命令,我只能告诉你,你可以收拾东西回去了,辞职报告也不用写了。那么应该怎么合理的删除缓存呢,接着往下看:
背景
项目开发中,我们经常把热点数据放入缓存,并且为了防止缓存雪崩,我们还设置了永不过期,但是在项目迭代,升级过程中就有一些场景需要更新缓存。比如我们新增了数据库的字段信息,业务数据有变更等。那么我们是去更新缓存还是去删除缓存呢,个人认为更新缓存没有太大的必要性,我们只需要去删除缓存即可,下次查询自动存入数据库。
难点
一些不了解redis特性的小伙伴们张口就来,简单。先keys pattern模糊所有要删除的key,再删除即可。看似没有毛病的,却隐藏的杀机。开发环境,测试环境并大量小,缓存数据量少的时候当然没啥问题,于是乎就高高兴兴的提交了代码,上了生产环境你就知道为什么不能这么做了。很多公司的运维已经禁用keys *命令,具体为什么请自行百度,我们今天讲另外一个实现方案。
SCAN命令
我们都知道redis是单线程的,keys*是阻塞的方式,时间复杂度是O(N),因此在生产稍微不注意就阻塞进程,造成redis卡顿,对于并发量大的系统是绝对不允许的。scan命令则是以非阻塞的方式,大多数情况下是可以替代keys命令的,scan提供类似limit的功能告诉redis每次查询多少条,scan在控制台的命令如下:
scan cursor [MATCH pattern] [COUNT count]
我的本地redis有3000条数据,其中有1000条是com:yangyao:开头的key,请看下面的scan语句,每次扫描10条,返回的不一定有10条,redis只能尽力去保证,每次scan后会返回一个游标,用于下次接着scan
127.0.0.1:6379> scan 0 match com:yangyao:* count 101) "1280"2) 1) "com:yangyao:test:35"2) "com:yangyao:test:622"3) "com:yangyao:test:476"4) "com:yangyao:test:896"127.0.0.1:6379> scan 1280 match com:yangyao:* count 101) "640"2) 1) "com:yangyao:test:113"2) "com:yangyao:test:441"3) "com:yangyao:test:700"
RedisTemplate实战
/*** 根据key和count扫描** @param keys* @param scanCount* @param consumer* @return key列表*/private void scanKeys(String keys, Long scanCount, Consumer<byte[]> consumer) {this.redisTemplate.execute(new RedisCallback() {@Overridepublic Object doInRedis(RedisConnection redisConnection) throws DataAccessException {try (Cursor<byte[]> cursor = redisConnection.scan( new ScanOptions.ScanOptionsBuilder().count(scanCount).match(keys+"*").build())) {cursor.forEachRemaining(consumer);} catch (IOException e) {e.printStackTrace();throw new RuntimeException(e);}return null;}});}
写一个方法测试,实现思路是利用redisTemplate的execute执行scan扫描,将扫描的keys放入一个集合,再删除这些数据。
@GetMapping(value = "/removeTestCache")public String removeTestCache(@RequestParam("prefixKey") String prefixKey,@RequestParam("scanCount") Long scanCount) {List<String> keyList = new ArrayList<>();//调用我们封装的方法this.scanKeys(prefixKey, scanCount, item -> {String key = new String(item, StandardCharsets.UTF_8);keyList.add(key);});Long result = this.redisTemplate.delete(keyList);return result + "条数据被删除";}
测试
输入完成的url,调用删除方法,我们这里传入2个参数,prefixKey是key的前缀,scanCount是每次扫描多少条数据,可以看到我们只需要调用一次,就可以把我们模糊的key都删除掉。很多人以为scanCount每次查询10条是不是需要多次调用,知道返回0,其实是不需要的哈。

monitor
我们只调用了一次,其实redis已经把所有的key扫描完了,看看redis为我们做了什么,可以再redis-cli命名输入monitor之后,再请求一次删除的命令,我这里拷贝了部分,其实就是为我们执行了多次scan命令,每次的count就是我们传入的10次,所有我们方法只需要调用一次就可以了
127.0.0.1:6379> monitorOK1590071648.140816 [0 127.0.0.1:59965] "SCAN" "0" "MATCH" "com*" "COUNT" "10"1590071648.148479 [0 127.0.0.1:59965] "SCAN" "1280" "MATCH" "com*" "COUNT" "10"1590071648.149434 [0 127.0.0.1:59965] "SCAN" "640" "MATCH" "com*" "COUNT" "10"1590071648.150195 [0 127.0.0.1:59965] "SCAN" "3968" "MATCH" "com*" "COUNT" "10"1590071648.150849 [0 127.0.0.1:59965] "SCAN" "1856" "MATCH" "com*" "COUNT" "10"1590071648.151542 [0 127.0.0.1:59965] "SCAN" "544" "MATCH" "com*" "COUNT" "10"1590071648.152210 [0 127.0.0.1:59965] "SCAN" "2848" "MATCH" "com*" "COUNT" "10"1590071648.152874 [0 127.0.0.1:59965] "SCAN" "2400" "MATCH" "com*" "COUNT" "10"1590071648.153477 [0 127.0.0.1:59965] "SCAN" "2784" "MATCH" "com*" "COUNT" "10"1590071648.154229 [0 127.0.0.1:59965] "SCAN" "1552" "MATCH" "com*" "COUNT" "10"1590071648.154704 [0 127.0.0.1:59965] "SCAN" "3216" "MATCH" "com*" "COUNT" "10"1590071648.155499 [0 127.0.0.1:59965] "SCAN" "2128" "MATCH" "com*" "COUNT" "10"1590071648.156001 [0 127.0.0.1:59965] "SCAN" "208" "MATCH" "com*" "COUNT" "10"1590071648.156496 [0 127.0.0.1:59965] "SCAN" "3536" "MATCH" "com*" "COUNT" "10"1590071648.156971 [0 127.0.0.1:59965] "SCAN" "304" "MATCH" "com*" "COUNT" "10"




