使用spring-data-redis对redis集群进行KEY模糊删除的几种方式

 环境:虚拟机3主3从

 准备工作:

 向redis cluster写入100W条数据

    public void multiSet() {
        long start = System.currentTimeMillis();
        //定义key对应的value
        Consumer consumer = new Consumer();
        consumer.setId(11111L);
        consumer.setName("multiSet-consumer-");
        consumer.setPhone("13011112222");
        consumer.setBirthday(new Date());
        byte[] trueValue = RedisSerializer.json().serialize(consumer);
        //start
        for (long k = 1; k <= 100; k++) {
            long kk = k * 10000;
            System.out.println(String.format("第【%s】次批量添加", k));
            redisTemplate.executePipelined((RedisCallback) connection -> {
                for (long x = kk - 9999; x <= kk; x++) {
                    String key = "multiSet-Key-" + x;
                    byte[] trueKey = RedisSerializer.string().serialize(key);
                    connection.set(trueKey, trueValue);
                    System.out.println("*****" + x + "*****");
                }
                return null;
            });
        }
        long end = System.currentTimeMillis();
        System.out.println(String.format("总计耗时:【%s】秒", (end - start) / 1000));
    } 
  

运行结果:

集群状态:

一、使用del xxx*

    public void deleteOne() {
        Boolean result = redisTemplate.delete("multiSet-Key-*");
        System.out.println("***********" + result);
    }

运行结果:

此方法无效,说明redis不支持del xxx*

二、使用keys方法拿出所有key,然后delete所有key

    public void deleteTwo() {
        Long result = redisTemplate.delete(redisTemplate.keys("multiSet-Key-*"));
        System.out.println("***********" + result);
    }

运行结果:命令执行超时异常,redis中所有key依然健在

集群状态:

注意:

    1、如果key很少,是不会报异常的,而且key会被删除

    2、但是无论删除的KEY少还是多,方法都会一直阻塞住无法结束,就是keys()方法导致的

    3、同时redis集群性能严重下降,get set耗时较长

此做法不OK

三、使用lua脚本,用scan分批次扫描出key,然后进行删除

    public void deleteThree() {
        long start = System.currentTimeMillis();
        String luaScript =
                "while(true) " +
                        "do local x = redis.call('scan','0','match',KEYS[1]) " +
                        "for i,k in ipairs(x[2]) " +
                        "do redis.call('del',k) " +
                        "end " +
                        "if(x[1] == '0') then break end " +
                        "end";
        DefaultRedisScript defaultRedisScript = new DefaultRedisScript(luaScript);
        ScriptExecutor scriptExecutor = new DefaultScriptExecutor(redisTemplate);
        scriptExecutor.execute(defaultRedisScript, Arrays.asList("multiSet-Key-*"));
        long end = System.currentTimeMillis();
        System.out.println(String.format("总计耗时:【%s】秒", (end - start) / 1000));
    }

运行结果:命令执行超时异常,redis集群状态异常

Exception in thread "main" org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s)

有个redis master已经不可用了

(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.

192.168.8.132已挂

集群状态:出问题了

使用spring-data-redis对redis集群进行KEY模糊删除的几种方式_第1张图片

注意:如果key较少

    1、是不会报异常的,而且key会被删除

    2、但是只能删除一个master上的数据,其他master上的匹配KEY没有被删

    3、被删的那个master执行脚本期间,不可读写

此做法不OK

集群恢复解决方法:

    1、在出问题的master执行redis命令script kill

    2、在出问题的master执行redis命令shutdown nosave

    3、集群所有机器执行killall redis-server

    4、集群所有机器重新启动redis

四、使用 scan + pipeline 批量删除

    public void deleteFour() {
        long start = System.currentTimeMillis();
        //获取所有KEY怼到set中
        RedisCallback> redisCallback = connection -> {
            Set keySet = new HashSet<>();
            ScanOptions.ScanOptionsBuilder scanOptionsBuilder = ScanOptions.scanOptions();
            scanOptionsBuilder.match("multiSet-Key-*");
            ScanOptions scanOptions = scanOptionsBuilder.build();
            Cursor cursor = connection.scan(scanOptions);
            int count = 0;
            while (cursor.hasNext()) {
                keySet.add(RedisSerializer.string().deserialize(cursor.next()));
                count++;
            }
            System.out.println("获取到的KEY总量为:" + count);
            return keySet;
        };
        Set keySet = (Set) redisTemplate.execute(redisCallback);
        //遍历set中的KEY,进行分批次删除,一次10000条
        List list = new ArrayList<>();
        for(String s : keySet){
            if(list.size() == 10000){
                redisTemplate.executePipelined((RedisCallback) connection -> {
                    for(String key : list){
                        connection.del(RedisSerializer.string().serialize(key));
                    }
                    return null;
                });
                list.clear();
            }
            list.add(s);
        }
        if(list.size() > 0){
            redisTemplate.executePipelined((RedisCallback) connection -> {
                for(String key : list){
                    connection.del(RedisSerializer.string().serialize(key));
                }
                return null;
            });
        }
        long end = System.currentTimeMillis();
        System.out.println(String.format("总计耗时:【%s】秒", (end - start) / 1000));
        System.out.println("清除所有指定前缀KEY执行完毕--------------------");
    } 
  

运行结果:

TIPS1、如果所有redis服务当前不是很忙,同时管道批量操作的数量不是很大,则一切OK;其他线程 set get del等都正常;

TIPS2、删除期间,当有其他线程在get值的时候,有一定几率产生异常;同时其他线程操作也会发生异常

此方法虽然效率高,但是还是存在一定风险.

五、使用 scan + redisTemplate 一个个删

    public void deleteFive() {
        long start = System.currentTimeMillis();
        RedisCallback> redisCallback = connection -> {
            Set keySet = new HashSet<>();
            ScanOptions.ScanOptionsBuilder scanOptionsBuilder = ScanOptions.scanOptions();
            scanOptionsBuilder.match("multiSet-Key-*");
            ScanOptions scanOptions = scanOptionsBuilder.build();
            Cursor cursor = connection.scan(scanOptions);
            int count = 0;
            while (cursor.hasNext()) {
                keySet.add(RedisSerializer.string().deserialize(cursor.next()));
                count++;
            }
            System.out.println("获取到的KEY总量为:" + count);
            return keySet;
        };
        Set keySet = (Set) redisTemplate.execute(redisCallback);
        Iterator it = keySet.iterator();
        while (it.hasNext()) {
            redisTemplate.delete(it.next());
        }
        long end = System.currentTimeMillis();
        System.out.println(String.format("总计耗时:【%s】秒", (end - start) / 1000));
        System.out.println("清除所有指定前缀KEY执行完毕--------------------");
    }

运行结果:

集群状态

确实清除了所有KEY

redis集群性能未受到任何影响,set get del 等操作都正常

但是KEY怼在一个set里面还是不太合适

六、使用 scan + connection 一边获取一边删除(最优方案)

    public void deleteSix() {
        long start = System.currentTimeMillis();
        RedisCallback redisCallback = connection -> {
            ScanOptions.ScanOptionsBuilder scanOptionsBuilder = ScanOptions.scanOptions();
            scanOptionsBuilder.match("multiSet-Key-*");
            ScanOptions scanOptions = scanOptionsBuilder.build();
            Cursor cursor = connection.scan(scanOptions);
            long count = 0;
            while (cursor.hasNext()) {
                count += connection.del(cursor.next());     //count为所有删除成功次数的总和
                System.out.println("******" + count + "******");
            }
            return count;
        };
        Long count = (Long) redisTemplate.execute(redisCallback);
        long end = System.currentTimeMillis();
        System.out.println("被删除KEY的数量为:" + count);
        System.out.println(String.format("总计耗时:【%s】秒", (end - start) / 1000));
    }

运行结果:

集群状态:

发现KEY全部清除了

优势:

  1. redis集群性能未受到任何影响,set get del 等操作都正常
  2. 不需要将获取到的key都保存在set中再取出来删,节约服务器内存
  3. 代码简单

虽然执行时间很长,但是这种缓和处理占用资源很少,对redis集群不会造成压力。

方法重新整理一下:

    public boolean deleteSix(String pattern) {
        RedisCallback redisCallback = connection -> {
            try{
                ScanOptions.ScanOptionsBuilder scanOptionsBuilder = ScanOptions.scanOptions();
                scanOptionsBuilder.match(pattern);
                ScanOptions scanOptions = scanOptionsBuilder.build();
                Cursor cursor = connection.scan(scanOptions);
                while (cursor.hasNext()) {
                    connection.del(cursor.next());
                }
                return true;
            }catch (Exception e){
                return false;
            }
        };
        return (boolean) redisTemplate.execute(redisCallback);
    }

总结:

    1、如果想迅速进行批量删除,可采用方案4(redisTemplate scan + pipeline),云服务器的性能强悍,

          应该不会那么容易卡顿吧...SO 管道流操作可以发挥它的优势。

    2、对于缓存,可能有批量清除的需求,此时可以使用方案6,让他删个几十分钟也不过分。

          And,缓存我推荐使用自定义缓存,非永久化的数据每个KEY都设置过期时间。这样就用不着批量删除了。

你可能感兴趣的:(使用spring-data-redis对redis集群进行KEY模糊删除的几种方式)