删除 Redis 大 Key 让程序出现雪崩导致程序员被开除!

/ Redis / 没有评论 / 1325浏览

删除 Redis 大 Key 让程序出现雪崩导致程序员被开除!

寒冬了,也到年底了,大家都不要报任何侥幸心理。最近的寒冬,大家都知道,各个公司都在想办法进行人员优化。各位千万别出差错,别给公司找麻烦,给想开除你的人找借口!最近我们公司就发生了一件事,一个误操作,导致生产上的应用发生了雪崩,开发人员被迫离职。

今天我们就来复盘一下,这个误操作。

我们都知道,大公司的权限管理比较集中,而小公司呢?权限管理就比较分散。导致开发人员和运维人员的权限较大,一不小心就出故障。

我们的这位开发人员,根据领导的要求,想在生产上 Redis 系统删除一个 Key,但是这个 Key 太大了,存在设计不合理的现象。这位开发人员接到命令后,直接在生产环境上进行了大 Key 的删除,导致 Redis 进程被阻塞了十几秒,最终导致集群的容量和请求出现”倾斜问题“。在加上应用程序本身架构的就缺乏合理性,引起了雪崩。一时间客服电话被打爆了。

什么是 Redis 的大健?

每个人可能都有不同的理解,但我认为可以从空间复杂性和时间复杂度两个方面定义大 Key。

空间复杂性,指的就是占用的内存大小。例如一个 String 键最大为 512MB。你的 String 键有 300MB,这就算一个大 Key。

时间复杂度,指的就是一个键,存储的字段太多。比如一个 Hash 键,你存储了 100000000 个字段,这也可以算一个大 Key。因为它的对应访问模式(如 hgetall )时间复杂度高。

但是 String 键虽然算一个大 Key,我们直接操作内存的话,del 200MB String 键耗时约 1 毫秒,还算比较快的,影响不大。

但是你要删除 1kw 个字段的 Hash 键,那么就需要特别小心了,直接删除就会阻塞 Redis 进程数十秒左右的时间。

Redis 官网上针对删除 DEL key 命令,有这样一句话:

Time complexity: O(N) where N is the number of keys that will be removed. When a key to remove holds a value other than a string, the individual complexity for this key is O(M) where M is the number of elements in the list, set, sorted set or hash. Removing a single key that holds a string value is O(1).

大致的意思如下:

时间复杂度:O(N),其中N是将被删除的密钥数。当要删除的键持有字符串以外的值时,此键的单独复杂度是O(M),其中M是列表、集合、排序集或散列中的元素数量。移除包含字符串值的单个键是O(1)。

说了这么多,那么该如何正确的删除大 Key 呢?

这个问题我记得 3 群里有人曾经问过我。今天我再来回答一下!

处理 Redis 大 Key

合理的使用 Redis 的过期策略,redis 中对于有设置过期的 key 有三种处理方式:

如果你的应用程序设计不合理,key 越来越大,想要合理的删除 key,就需要循环各个 key,一个一个的删。这样 Redis 就不会被长期的阻塞,导致意外发生。

Large Hash Key 可使用 hscan 命令,每次获取 500 个字段,再用 hdel 命令,每次删除 1 个字段。

Large Set Key 可使用 sscan 命令,每次扫描集合中 500 个元素,再用 srem 命令每次删除一个键。

Large List Key 可通过 ltrim 命令每次删除少量元素。

Large Sorted Set Key 使用 sortedset 自带的 zremrangebyrank 命令,每次删除 top 100 个元素。

下面我以 hscan 为例,给大家看一个例子。

public static void delLargeHashKey(Jedis jedis){
    // 游标初始值为0
    String cursor = ScanParams.SCAN_POINTER_START;
    ScanParams scanParams = new ScanParams();
    scanParams.count(1000);
    String key = "test:xttblog";
    while (true){
        //使用hscan命令获取500条数据,使用cursor游标记录位置,下次循环使用
        ScanResult<Map.Entry<String, String>> hscanResult = jedis.hscan(key, cursor, scanParams);
        cursor = hscanResult.getStringCursor();// 返回0 说明遍历完成
        List<Map.Entry<String, String>> scanResult = hscanResult.getResult();
        long t1 = System.currentTimeMillis();
        for(int m = 0;m &lt; scanResult.size();m++){
            Map.Entry<String, String> mapentry = scanResult.get(m);
            jedis.hdel(key, mapentry.getKey());
        }
        long t2 = System.currentTimeMillis();
        System.out.println("删除" + scanResult.size() + "条数据,耗时: " + (t2-t1) + "毫秒,cursor:" + cursor);
        if ("0".equals(cursor)){
            break;
        }
    }
}

其他几个用法都类似,我就不在列举了。

1

参考上面的图,当你的 Key 在这个范围的,删除 Key 就要小心了。当然我的这个截图和大家的硬件环境之类的不一样,可能略有差异,仅供参考!