Java 面试宝典:什么是大 key 问题?如何解决?

2 阅读5分钟

大家好,我是大明哥,一个专注「死磕 Java」系列创作的硬核程序员。 本文已收录到我的技术网站:skjava.com。有全网最优质的系列文章、Java 全栈技术文档以及大厂完整面经


回答

Redis 大 key 问题是指某个 key 对应的 value 值很大(注意,不是 key 很大)。大 key 会导致 Redis 性能降低、数据倾斜以及主从同步等问题。一般来说我们应该杜绝大 key,如果遇到了我们就需要对其进行处理,处理过程分为这两个步骤:

  1. 发现大 key。比如利用 redis-cli --bigkeys 或者 Redis RDB Tools

  2. 大 key 的治理方案一般分为两种:

    1. 可删除:使用 UNLINK 命令可以安全地删除大 key。
    2. 不可删除:不可删除的话就将大 key 拆分为多个小 key,或者对 value 进行压缩处理

详解

什么是大 key

首先大 key 不是 key 很大,而是 key 所对应的 value 很大。如果 value 超过某个阈值,那么此时存储这个 value 所对应的 key 就是大 key。

那 value 多大才算大 key 呢?这个阈值没有一个衡量的标准,需要根据具体场景来确定,比如有些场景及时 KB 是大 key,而有些场景需要几十 MB。

通常情况下,Redis 官方提到的大 key 阈值是基于经验值,例如:

  • 对于列表、集合、有序集合、 哈希表,在超过 1 万个元素时被认为是大 key。
  • 对于字符串,当它的大小达到几百 KB 时可能被认为是大 key。

为了后面演示,我们通过程序向 Redis 中插入一批数据,表格如下:

  • 字符串
key数据量
bigkey-0130 个 "skjava" 拼接
bigkey-02300 个 "skjava" 拼接
bigkey-033000 个 "skjava" 拼接
bigkey-0430000 个 "skjava" 拼接
bigkey-0560000个 "skjava" 拼接
bigkey-0690000 个 "skjava" 拼接
bigkey-07120000个 "skjava" 拼接

程序如下(数字各位小伙伴就去改下):

    @Test
    public void bigKeyTest() {
        StringBuffer stringBuffer_01 = new StringBuffer();
        for (int i = 0; i < 30 ; i++) {
            stringBuffer_01.append("skjava-" + i + ";");
        }
        
        // 省略

        StringBuffer stringBuffer_07 = new StringBuffer();
        for (int i = 0; i < 120000 ; i++) {
            stringBuffer_07.append("skjava-" + i + ";");
        }

        Jedis jedis = new Jedis("127.0.0.1",6379);
        jedis.set("bigkey-01",stringBuffer_01.toString());
        //...
        jedis.set("bigkey-07",stringBuffer_07.toString());
    }

各个 key 大小如下图:

  • 列表
key数据量
bigkey-0830 个 "skjava"
bigkey-09300 个 "skjava"
bigkey-103000 个 "skjava"
bigkey-1130000 个 "skjava"
bigkey-1260000个 "skjava"
bigkey-1390000 个 "skjava"
bigkey-14120000个 "skjava"

代码

    @Test
    public void bigKeyTest() {
        List<String> list_08 = new ArrayList<>();
        for (int i = 0 ; i < 30 ; i++) {
            list_08.add("skjava-" + i);
        }

        // ...

        List<String> list_14 = new ArrayList<>();
        for (int i = 0 ; i < 120000 ; i++) {
            list_14.add("skjava-" + i);
        }

        Jedis jedis = new Jedis("127.0.0.1",6379);
        jedis.lpush("bigkey-08", list_08.toArray(new String[list_08.size()]));
        // ...
        jedis.lpush("bigkey-14", list_14.toArray(new String[list_14.size()]));
    }

列表长度及大小:

大 key 有什么危害

  • 数据倾斜

大 key 所在的 Redis 服务器,会比其他 Redis 服务器占用更多的内存,这种数据倾斜的现象明显违背 Redis-Cluster 的设计思想。

  • 占用内存过高

大 key 会占用更多的内存,可能会导致内存不足,甚至导致内存耗尽,Redis实例崩溃,影响系统的稳定性。

同时,频繁对大 key 进行修改可能会导致内存碎片化,进一步影响性能。

  • 性能下降

大 key 会占用大量的内存,导致内存碎片增加,进而影响Redis的性能。同时,对大 key 的读写操作消耗的时间都会比较长,这会导致单个操作阻塞 Redis 服务器,影响整体性能。

尤其是执行像 HGETALLSMEMBERSZRANGELRANGE 等命令时,如果操作的是大 key,可能会导致明显的延迟。

  • 主从同步延迟

如果配置了主从同步,大 key 会导致主从同步延迟,由于大 key 占用的内存比较大,当主节点上的大 key 发生变化时,同步到从库时,会导致网络和处理上的延迟。

如何找到大 key

1、--bigkeys

在使用redis-cli命令链接 Redis 服务的时候,加上 --bigkeys 参数,就可以找出每种数据类型的最大 key 及其大小。

bigkeys 使用的是 SCAN 来迭代 Redis 中所有的 key,对于每种数据类型(字符串、列表、集合、有序集合、哈希表),它都会记录下占用最多内存的 key。但是该操作是资源密集型的,不建议直接在生产上面执行,建议在从节点或者低流量时段执行。

这种方式对集合类型来说不是特别友好,因为它只统计集合元素的多少,而不是实际占用内存,但是集合元素多,并不代表占用内存大。

2、SCAN命令

SCAN 命令结合 MEMORY USAGESCAN 可以迭代数据库中的 key,然后结合 MEMORY USAGE 命令检查每个 key 的大小。

redis-cli --scan --pattern '*' | xargs -L 1 -I '{}' sh -c 'echo {} && redis-cli memory usage {}'

这个命令会显示每个 key 的内存使用量:

  • redis-cli --scan --pattern '*':遍历所有的 key。
  • xargs -L 1 -I '{}' sh -c 'echo {} && redis-cli memory usage {}':对于每个 key,首先打印 key 的名称(echo {}),然后打印其内存使用情况(redis-cli memory usage {}

3、Redis RDB Tools工具

RdbTools 是一个用于分析 Redis 数据库文件(.rdb 文件)的工具。它可以帮助我们理解 Redis 内存使用情况,找出大 key,并以此优化 Redis 实例。

例如,我们要找出占用内存最多的 5 个 key:

rdb --command memory --largest 5 dump.rdb

再如,获取字节数大于 10000 的 key:

rdb --command memory --bytes 10000 dump.rdb

关于Redis RDB Tools 的安装与使用:www.cnblogs.com/zyf98/p/156…

如何处理大 key

如何处理大 key 呢?方案有如下两类:

1、可删除

如果这些大 key 可以删除,则我们可以使用 UNLINK 删除这些大 key。UNLINK 是异步的,不会像 DEL 命令一样会阻塞 Redis 服务,这种特性使得它比较适合大型的列表、集合、散列或有序集合的删除。

2、不可删除

对于不可删除的大 key,我们一般有两种方式出路:

  1. 分解。将大 key 拆分为多个小 key,降低单key的大小,读取可以用mget批量读取。
  2. 压缩。如果是 String 类型,我们可以采用压缩算法对其进行压缩处理。