Redis7——进阶篇(二)

发布于:2025-03-05 ⋅ 阅读:(38) ⋅ 点赞:(0)

 前言:此篇文章系本人学习过程中记录下来的笔记,里面难免会有不少欠缺的地方,诚心期待大家多多给予指教。


基础篇:

  1. Redis(一)
  2. Redis(二)
  3. Redis(三)
  4. Redis(四)
  5. Redis(五)
  6. Redis(六)
  7. Redis(七)
  8. Redis(八)

进阶篇:

  1. Redis(九)

接上期内容:上期完成了Redis单多线程以及IO多路复用方面的学习。下面开始学习redis处理Bigkey和MoreKey的知识,话不多说,直接发车。


一、什么是BigKey、MoreKey

(一)、BigKey定义

BigKey,简单来说,就是指那些值(value)特别大的键(key)。这里的 “大”,主要体现在两个方面:

  • 数据大小对于字符串类型的键值对,如果单个value值很大,一般认为超过10KB就是 BigKey。(参考阿里云Redis开发规范)
  • 成员数量当键对应的值是哈希、列表、集合、有序集合等非字符串类型时,如果这些数据结构中的元素个数太多,也会被视为 BigKey,一般认为元素超过5000个就是BigKey。(参考阿里云Redis开发规范)

(二)、MoreKey定义

MoreKey通常指的是当数据库中的key数量非常多时,使用如KEYS *这样的命令去检索所有的 key,会导致 Redis 服务阻塞,影响正常业务。


二、BigKey如何产生和危害?

(一)、BigKey如何产生?

  1.  业务规划不合理:在业务开发初期,如果缺乏对数据规模和访问模式的充分预估,可能会导致将大量相关数据存储在一个 key 中。
  2. 数据结构使用不当:Redis 的 String 类型虽然简单易用,但如果用它来存储大体积的二进制文件(如图片、视频的二进制数据)或者非常长的文本数据,就容易形成 BigKey。
  3. 数据清理机制失效或缺失:如果系统没有建立有效的数据清理机制或机制失效,过期或不再使用的数据会一直占用内存,导致 key 的大小不断增长。

(二)、BigKey产生后造成的危害

  1. 影响性能:当一个key是BigKey,那么对这个key的读、写都是会带来严重的性能问题。如果这个key足够大,甚至会阻塞redis服务器。
  2. 占用内存:BigKey会占用大量的连续内存空间。如果一个redis集群中,存在大量的Bigkey,还可能导致整个集群的负载不均衡。
  3. 影响业务稳定性:BigKey 的存在可能会增加数据一致性维护的难度。当对 BigKey 进行更新操作时,如果操作过程中出现异常(如网络中断、Redis 服务器故障等),可能会导致部分数据更新成功,部分失败,从而破坏数据的一致性,影响数据一致性。

三、MoreKey、BigKey实操

(一)、MoreKey实操

1、模拟redis多key场景

通过脚本,生成1000万条redis命令

for((i=1;i<=1000*10000;i++)); do echo "set k$i v$i" >> /myredis/temp.txt ;done;

 在通过--pipe命令批量往redis里面存入1000万key。


2、keys *命令的弊端

如果在1000w的redis中,使用keys * 命令来查看某个key是否存在,结果是啥我就不多说。

如果在生产上,redis阻塞74s,是什么概念!!(⊙o⊙)

keys *,这个指令没有 ofset、limit 参数,是要一次性吐出所有满足条件的 key,由于 redis 是单线程的,其所有操作都是原子的,而 keys 算法是遍历算法,复杂度是 0(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,可能会引起缓存雪崩甚至数据库宕机。


3、SCAN命令实操

3.1、Q & A

Q:但是又想要查看或者查找某个key怎么做?

A:使用scan命令。

SCAN cursor [MATCH pattern] [COUNT count]

参数说明: 

  • cursor
    • 这是一个必需的参数,是一个整数值,作为游标使用。首次调用scan命令时,cursor要设置为 0,表示从数据库的起始位置开始迭代。每次调用 SCAN 命令后,会返回一个新的游标值,后续的迭代需要使用这个新的游标值继续进行,直到返回的游标值为 0,这意味着迭代结束。
  • [MATCH pattern]
    • 可选参数,用于指定键的匹配模式。可以使用通配符,如 * 表示匹配任意数量的任意字符,?表示匹配单个任意字符。例如,MATCH user:* 会返回所有以 user: 开头的键。
  • [COUNT count]
    • 可选参数,用于提示 Redis 每次迭代要返回的键的大致数量。不过,这只是一个提示,Redis 并不一定会严格按照这个数量返回键。通常,count的默认值是 10。

返回结果:

  • 第一个元素是用于进行下一次迭代的新游标。
  • 第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。如果新游标返回零表示迭代已结束。

*注意:SCAN的遍历顺序非常特别,它不是从第一维数组的第零位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。


3.2、scan的使用


(二)、BigKey实操

1、发现BigKey

①、--Bigkeys命令

bigkeys命令能给出每种数据结构,同时给出每种数据类型的键值个数+平均大小。但是它没法给出每个具体key的大小。

redis-cli -a 密码 --bigkeys


②、memory usage命令
MEMORY USAGE key

    MEMORY USAGE 命令给出一个 key 和它的值在 RAM 中所占用的字节数。返回的结果是 key 的值以及为管理该 key 分配的内存总字节数。

    这不妥妥的BigKey么。。。


    2、删除BigKey

    ①、第一种情况

    如果key为String类型,普通删除用del即可,BigKey使用unlink。


    ②、第二种情况

    非字符串的bigkey,不要使用del删除,而使用hscan、sscan、zscan方式渐进式删除。

    删除Hash:使用hscan每次获取少量field-value,再使用hdel删除每个field,hscan+hdel。

    public void delBigHash(String host, int port, String password, String bigHashKey) {
        Jedis jedis = new Jedis(host, port);
        if (password != null && !"".equals(password)) {
            jedis.auth(password);
        }
        ScanParams scanParams = new ScanParams().count(100);
        String cursor = "0";
        do {
            ScanResult<Entry<String, String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
            List<Entry<String, String>> entryList = scanResult.getResult();
            if (entryList != null && !entryList.isEmpty()) {
                for (Entry<String, String> entry : entryList) {
                    jedis.hdel(bigHashKey, entry.getKey());
                }
            }
            cursor = scanResult.getStringCursor();
        } while (!"0".equals(cursor));
        //删除bigkey
        jedis.del(bigHashKey);
    }

    删除Set:使用sscan每次获取部分元素,再使用srem命令删除每个元素,sscan + srem。

    public void delBigSet(String host, int port, String password, String bigSetKey) {
        Jedis jedis = new Jedis(host, port);
        if (password != null && !"".equals(password)) {
            jedis.auth(password);
        }
        ScanParams scanParams = new ScanParams().count(100);
        String cursor = "0";
        do {
            ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
            List<String> memberList = scanResult.getResult();
            if (memberList != null && !memberList.isEmpty()) {
                for (String member : memberList) {
                    jedis.srem(bigSetKey, member);
                }
            }
            cursor = scanResult.getStringCursor();
        } while (!"0".equals(cursor));
        //删除bigkey
        jedis.del(bigSetKey);
    }

    删除List:使用ltrim渐进式逐步删除,直到全部删除完成。

    public void delBigList(String host, int port, String password, String bigListKey) {
        Jedis jedis = new Jedis(host, port);
        if (password != null && !"".equals(password)) {
            jedis.auth(password);
        }
        long llen = jedis.llen(bigListKey);
        int counter = 0;
        int left = 100;
        while (counter < llen) {
            //每次从左侧截掉100个
            jedis.ltrim(bigListKey, left, llen);
            counter += left;
        }
        //最终删除bigKey
        jedis.del(bigListKey);
    }

    删除zSet: 使用zscan每次获取部分元素,再使用zrem命令删除每个元素,zscan+zrem。

    public void delBigZset(String host, int port, String password, String bigZsetKey) {
        Jedis jedis = new Jedis(host, port);
        if (password != null && !"".equals(password)) {
            jedis.auth(password);
        }
        ScanParams scanParams = new ScanParams().count(100);
        String cursor = "0";
        do {
            ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);
            List<Tuple> tupleList = scanResult.getResult();
            if (tupleList != null && !tupleList.isEmpty()) {
                for (Tuple tuple : tupleList) {
                    jedis.zrem(bigZsetKey, tuple.getElement());
                }
            }
            cursor = scanResult.getStringCursor();
        } while (!"0".equals(cursor));
        //删除bigkey
        jedis.del(bigZsetKey);
    }

    四、BigKey调优

    (一)、惰性删除

    惰性删除(Lazy Free)是 Redis 为了优化内存释放操作、避免阻塞主线程而引入的一种机制。在 Redis 中,键的删除操作可能会涉及大量内存的释放,如果这些操作都在主线程中同步执行,可能会导致主线程阻塞,影响 Redis 的性能和响应能力。


    (二)、配置项解释

    解释:

    • lazyfree-lazy-eviction控制当 Redis 执行内存淘汰策略时,是否采用异步方式释放被淘汰键占用的内存。设置为 yes 时,采用异步方式;设置为 no 时,采用同步方式。
    • lazyfree-lazy-expire当 Redis 中的键过期时,是否使用异步方式删除过期键。设置为 yes 时,异步删除;设置为 no 时,同步删除。
    • lazyfree-lazy-server-del控制在使用 del、unlink 等命令删除键时,是否采用异步方式释放键占用的内存。设置为 yes 时,异步释放;设置为 no 时,同步释放。
    • replica-lazy-flush:用于控制从节点在进行全量同步时,是否使用异步方式清空数据库。设置为 yes 时,异步清空;设置为 no 时,同步清空
    • lazyfree-lazy-user-del控制用户使用 DEL 命令删除键时,是否采用异步方式释放键占用的内存。设置为 yes 时,del 命令会以异步方式工作,类似于 unlink 命令;设置为 no 时,DEL 命令保持默认的同步删除行为。
    • lazyfree-lazy-user-flush此配置项用于控制在用户执行 FLUSHDBFLUSHALLSCRIPT FLUSH 和 FUNCTION FLUSH 命令,且未指定 SYNC 或 ASYNC 标志时,这些命令的数据删除操作是采用异步还是同步方式。设置为 yes 时,若执行上述命令且未指定删除模式时则采用异步清空;设置为 no 时,则采用同步清空。

    五、关于MoreKey、BigKey的经典面试题

    (一)、在海量数据中如何查找固定前缀的key?如何遍历key?

    1、查找固定前缀的key

    • 针对于key类型为String,可以使用命令是 Redis 提供的用于渐进式迭代键空间的命令,可配合MATCH选项来查找具有固定前缀的 key。

    • 针对于hash、set、zset类型的数据,可以使用hscan、sscan、zscan命令来查找固定前缀的key。


    2、遍历key

    • 基本原理和查找固定前缀的 key 类似,只是不使用MATCH选项。通过不断迭代游标,逐步遍历整个键空间。


    (二)、如何在生产上限制keys*/flushdb/flushall等危险命令的使用?

    1、配置ACL

    从 Redis 6.0 版本开始,支持通过ACL(访问控制列表)来限制用户对特定命令的使用。例如:可以通过命令创建,

    # 创建一个新的用户,只允许执行部分安全命令
    ACL SETUSER test on > test ~* +get +set +del

    也可以在配置文件配置相

    2、rename-command

    可以通过redis重命名功能完全禁止某些命令。


    (三)、Memory usage命令有使用过吗?能干嘛?

    该命令用于返回一个键所占用的内存字节数,主要用途:

    ①、分析内存占用:了解每个键具体占用了多少内存,有助于找出占用大量内存的 bigkey,进行针对性的优化。

    ②、内存优化决策:根据键的内存使用情况,决定是否需要对键进行拆分、压缩或者删除等操作,以优化 Redis 的内存使用。


    (四)、什么是BigKey?多大算BigKey?如何发现、删除?

    1、什么是 BigKey

    BigKey 指的是在 Redis 中占用大量内存或者包含大量元素的键。


    2、多大算 BigKey

    并没有一个固定的标准来定义多大算 BigKey,在以下情况可以认为是 BigKey:

    ①、字符串类型键值大小超过10KB。

    ②、hash、list、set、zset等类型包含的元素数量超过 5000 个。


    3、如何发现 BigKey

    可以使用两个命令来发现BigKey:

    ①、Memory usage计算key占据内存大小。

    ②、--bigkeys:统计每个类型key占据内存情况。


    4、如何删除BigKey

    ①、推荐使用unlink命令异步删除。

    ②、开启异步删除配置后在使用DEL命令删除。

    ③、通过scan命令分批获取bigkey中的字段,再用del命令逐个删除这些字段,最后删除整个键。比如hash就是使用hscan+hdel来进行key的删除。


    (五)、BigKey调优之惰性删除

    Redis 的惰性删除机制是为了避免在删除大键时阻塞主线程。当执行删除操作时,主线程只负责标记该键为已删除,然后将释放内存的任务交给后台线程处理,主线程可以继续处理其他请求。Redis 提供了多个配置项来控制不同场景下的惰性删除行为,如lazyfree-lazy-eviction,lazyfree-lazy-expire,lazyfree-lazy-server-del等。通过将这些配置项设置为yes,可以开启相应场景下的惰性删除功能。


    六、总结

    在了解了 BigKey 和 MoreKey 相关知识后,我在实际工作里面对此类问题时能够更加从容地应对,处理起来游刃有余。而且在面试场景中,当遇到涉及这方面的问题时,我也更有底气,回答起来更加得心应手。


    ps:努力到底,让持续学习成为贯穿一生的坚守。学习笔记持续更新中。。。。