• Redis++:Redis 大 key的发现与删除方法全解析


    关于Redis大键(Key),我们从 [空间复杂性] 和访问它的 [时间复杂度] 两个方面来定义大键。

    前者主要表示Redis键的占用内存大小;后者表示Redis集合数据类型(set/hash/list/sorted set)键,所含有的元素个数。

    以下两个示例:

    1个大小200MB的String键(String Object最大512MB),内存空间占用较大;
    1个包含100000000(1kw)个字段的Hash键,对应访问模式(如hgetall)时间复杂度高

    因为内存空间复杂性处理耗时都非常小,测试 del 200MB String键耗时约1毫秒,而删除一个含有1kw个字段的Hash键,却会阻塞Redis进程数十秒。

    所以本文只从时间复杂度分析大的集合类键。删除这种大键的风险,以及怎么优雅地删除。

    在Redis集群中,应用程序尽量避免使用大键;直接影响容易导致集群的容量和请求出现”倾斜问题“,具体分析见文章:redis-cluster-imbalance

    但在实际生产过程中,总会有业务使用不合理,出现这类大键;当DBA发现后推进业务优化改造,然后删除这个大键;

    如果直接删除它,DEL命令可能阻塞Redis进程数十秒,对应用程序和Redis集群可用性造成严重的影响。

    直接删除大Key的风险

    DEL命令 在删除单个集合类型的Key时,命令的时间复杂度是O(M),其中M是集合类型Key包含的元素个数。

    DEL keyTime 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).

    生产环境中遇到过多次因业务删除大Key,导致Redis阻塞,出现故障切换和应用程序雪崩的故障。

    测试删除集合类型大Key耗时,一般每秒可清理100w~数百w个元素; 如果数千w个元素的大Key时,会导致Redis阻塞上10秒可能导致集群判断Redis已经故障,出现故障切换;或应用程序出现雪崩的情况。

    说明:Redis是单线程处理。单个耗时过大命令,导致阻塞其他命令,容易引起应用程序雪崩或Redis集群发生故障切换。所以避免在生产环境中使用耗时过大命令。

    Redis删除大的集合键的耗时, 测试估算,可参考;和硬件环境、Redis版本和负载等因素有关

    当我们发现集群中有大key时,要删除时,如何优雅地删除大Key?

    从Redis2.8版本开始支持SCAN命令,通过m次时间复杂度为O(1)的方式,遍历包含n个元素的大key.这样避免单个O(n)的大命令,导致Redis阻塞。

    这里删除大key操作的思想也是如此。

    说明:

      redis大key,这里指的是大的集合数据类型,如(set/hash/list/sorted set),一个key包含很多元素。

      由于redis是单线程,在删除大key(千万级别的set集合)的时候,或者清理过期大key数据时,主线程忙于删除这个大key,会导致redis阻塞、崩溃,应用程序异常的情况。

      redis-cli --bigkeys 命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key 、等其他方式

    一个栗子:↓

    线上redis作为实时去重的一个工具,里面有6千万的用户guid,这么一个set集合,如果直接使用del删除,会导致redis严重阻塞

    10.1.254.18:6380> info memory
    # Memory
    used_memory:15175740016
    used_memory_human:14.13G
    used_memory_rss:22302339072
    used_memory_peak:22351749192
    used_memory_peak_human:20.82G
    used_memory_lua:36864
    mem_fragmentation_ratio:1.47
    mem_allocator:jemalloc-3.6.0
    10.1.254.18:6380> scard helper_2019-03-12
    (integer) 64530980
    10.1.254.18:6380> del helper_2019-03-12
    (integer) 1
    (81.23s)
    10.1.254.18:6380> info memory
    # Memory
    used_memory:8466985704
    used_memory_human:7.89G
    used_memory_rss:10669453312
    used_memory_peak:22351749192
    used_memory_peak_human:20.82G
    used_memory_lua:36864
    mem_fragmentation_ratio:1.26
    mem_allocator:jemalloc-3.6.0

    可以看到,helper_2019-03-12这个key,是一个包含64530980个元素的集合,直接使用del删除命令,花的时间为:81.23s,显然会发送超时、阻塞,程序异常!

    这种情况,应该使用sscan命令,批量删除set集合元素的方法。下面是一个Java代码分批删除redis中set集合的例子:

    private static void test2(){
        // 连接redis 服务器
        Jedis jedis = new Jedis("0.0.0.0",6379);
        jedis.auth("123456");
    
        // 分批删除
        try {
            ScanParams scanParams = new ScanParams();
            // 每次删除 500 条
            scanParams.count(500);
            String cursor = "";
            while (!cursor.equals("0")){
                ScanResult<String> scanResult=jedis.sscan("testset", cursor, scanParams);
                // 返回0 说明遍历完成
                cursor = scanResult.getStringCursor();
                List<String> result = scanResult.getResult();
                long t1 = System.currentTimeMillis();
                for(int m = 0;m < result.size();m++){
                    String element = result.get(m);
                    jedis.srem("testset", element);
                }
                long t2 = System.currentTimeMillis();
                System.out.println("删除"+result.size()+"条数据,耗时: "+(t2-t1)+"毫秒,cursor:"+cursor);
            }
        }catch (JedisException e){
            e.printStackTrace();
        }finally {
            if(jedis != null){
                jedis.close();
            }
        }
    }

    对于其它集合,也有对应的方法。

    • hash key:通过hscan命令,每次获取500个字段,再用hdel命令;
    • set key:使用sscan命令,每次扫描集合中500个元素,再用srem命令每次删除一个元素;
    • list key:删除大的List键,未使用scan命令; 通过ltrim命令每次删除少量元素。
    • sorted set key:删除大的有序集合键,和List类似,使用sortedset自带的zremrangebyrank命令,每次删除top 100个元素。

    后台删除之lazyfree机制:

    为了解决redis使用del命令删除大体积的key,或者使用flushdb、flushall删除数据库时,造成redis阻塞的情况,在redis 4.0引入了lazyfree机制,可将删除操作放在后台,让后台子线程(bio)执行,避免主线程阻塞。

    lazy free的使用分为2类:第一类是与DEL命令对应的主动删除,第二类是过期key删除、maxmemory key驱逐淘汰删除。

    主动删除:

    UNLINK命令是与DEL一样删除key功能的lazy free实现。唯一不同时,UNLINK在删除集合类键时,如果集合键的元素个数大于64个(详细后文),会把真正的内存释放操作,给单独的bio来操作。

    127.0.0.1:7000> UNLINK mylist
    (integer) 1
    FLUSHALL/FLUSHDB ASYNC
    127.0.0.1:7000> flushall async //异步清理实例数据

    被动删除:

    lazy free应用于被动删除中,目前有4种场景,每种场景对应一个配置参数; 默认都是关闭。

    lazyfree-lazy-eviction no
    lazyfree-lazy-expire no
    lazyfree-lazy-server-del no
    slave-lazy-flush no

    lazyfree-lazy-eviction

      针对redis内存使用达到maxmeory,并设置有淘汰策略时;在被动淘汰键时,是否采用lazy free机制;

    因为此场景开启lazy free, 可能使用淘汰键的内存释放不及时,导致redis内存超用,超过maxmemory的限制。此场景使用时,请结合业务测试。

    lazyfree-lazy-expire

      针对设置有TTL的键,达到过期后,被redis清理删除时是否采用lazy free机制;

    此场景建议开启,因TTL本身是自适应调整的速度。

    lazyfree-lazy-server-del

      针对有些指令在处理已存在的键时,会带有一个隐式的DEL键的操作。如rename命令,当目标键已存在,redis会先删除目标键,如果这些目标键是一个big key,那就会引入阻塞删除的性能问题。

    此参数设置就是解决这类问题,建议可开启。

    slave-lazy-flush

      针对slave进行全量数据同步,slave在加载master的RDB文件前,会运行flushall来清理自己的数据场景,

    参数设置决定是否采用异常flush机制。如果内存变动不大,建议可开启。可减少全量同步耗时,从而减少主库因输出缓冲区爆涨引起的内存使用增长。

    expire及evict优化

      redis在空闲时会进入activeExpireCycle循环删除过期key,每次循环都会率先计算一个执行时间,在循环中并不会遍历整个数据库,

    而是随机挑选一部分key查看是否到期,所以有时时间不会被耗尽(采取异步删除时更会加快清理过期key),剩余的时间就可以交给freeMemoryIfNeeded来执行。

  • 相关阅读:
    因特网中和多媒体有关的协议
    进程与线程
    线程模型
    SMP PVP Cluster
    读写者
    回调函数
    环境变量
    堆与栈的区别
    操作系统中的同步、异步、阻塞和非阻塞
    Razor潜入2令人疑惑的LocateOwner方法
  • 原文地址:https://www.cnblogs.com/codingmode/p/15245010.html
Copyright © 2020-2023  润新知