欢迎光临
我们一直在努力

Redis 大 Key 排查实战:连踩两个工具的坑,最终用 SQL 揪出 3.3GB 的隐藏大户

线上 Redis 内存告警,used_memory 快打满了。你知道里面有大 Key,但不知道是谁、有多大、有多少个。

–bigkeys 只能告诉你每种类型最大的那一个,远远不够。redis-rdb-tools 一跑就报错——它不支持 Redis 7.4+ 的 RDB 12 格式。换了另一个工具,还是报错。

本文记录从踩坑到最终解决的完整过程:找到一个能解析 RDB 12 的工具,导出全量 Key 内存数据,导入 ClickHouse / MySQL 用 SQL 分析,最终揪出了 3.3GB 的隐藏大户。


一、大 Key 是什么,多大算大

1.1 定义

单个 Value 太大、单个容器元素太多,都叫大 Key。

1.2 多大算大?

官方没有统一标准(因为机器和网络环境不同),但业界有参考值:

来源标准
阿里云 Redis 最佳实践 Value > 10KB 算大(> 50KB 会记录慢日志)
京东云技术团队 String > 10KB;非 String(Hash/Set/ZSet/List)元素数 > 10,000 或总大小 > 100KB;集群 Key 总数 > 1 亿

简单记:String 超 10KB、集合超 1 万个元素,就该关注了。

1.3 大 Key 有什么危害

Redis 主工作线程是单线程,一次慢操作就会拖慢所有请求:

危害说明
阻塞主线程 一次 LRANGE 0 -1 读 100 万元素的 List,其他命令全等着
网络带宽打满 一个 200MB 的 Value 传输,网卡直接拉满
内存碎片 大 Key 删除后留下大块碎片,mem_fragmentation_ratio 飙升
主从同步延迟 大 Key 写入产生大量 RDB 传输
慢查询 DEL 一个大 Key 都可能阻塞几秒(Redis 4.0 前是同步删除)

二、–bigkeys 和 –memkeys 能做什么,不能做什么

2.1 –bigkeys:按元素数量找最大

redis-cli –bigkeys

⚠️ 生产环境慎用:–bigkeys 会执行全库 SCAN,Key 数量大时会持续占用 Redis 主线程,可能导致其他请求延迟。建议在从库或低峰期执行,或加 -i 0.1 参数(每 100 次 SCAN 间隔 0.1 秒)降低冲击。

它会扫描全库,找出每种类型元素数最多的 Key:

——– summary ——-

Sampled 1791571 keys in the keyspace!
Total key length in bytes is 65243446 (avg len 36.42)

Biggest list found "msg_wait_queue" has 203534 items
Biggest hash found "recharge_total_map" has 5616 fields
Biggest string found "ai_image_cache:198341999" has 534 bytes
Biggest set found "fans_account_set_20240404" has 17528 members
Biggest zset found "AUTO_LIKE_VIP" has 626255 members

130500 lists with 1886638 items (07.28% of keys, avg size 14.46)
259256 hashs with 2801460 fields (14.47% of keys, avg size 10.81)
765522 strings with 1195090 bytes (42.73% of keys, avg size 1.56)
135652 sets with 6993910 members (07.57% of keys, avg size 51.56)
500641 zsets with 7688654 members (27.94% of keys, avg size 15.36)

问题:它只看元素数量,不看内存大小。一个 626,255 个元素的 ZSet 可能占 70MB,但它不告诉你。

2.2 –memkeys:按内存大小找最大

redis-cli –memkeys –memkeys-samples 100

⚠️ 同样慎用于生产:–memkeys 对每个 Key 调用 MEMORY USAGE,比 –bigkeys 更慢更重。务必在从库执行。

比 –bigkeys 更进一步,用 MEMORY USAGE 命令估算每个 Key 的实际内存:

——– summary ——-

Biggest list found "msg_wait_queue" has 303666553 bytes # 289 MB!
Biggest hash found "recharge_total_map" has 335240 bytes # 327 KB
Biggest string found "ai_image_cache:198337364" has 720 bytes
Biggest set found "fans_account_set_20240404" has 963392 bytes # 940 KB
Biggest zset found "AUTO_LIKE_VIP" has 72217325 bytes # 68 MB

130500 lists with 356683505 bytes (07.28% of keys, avg size 2733.21)
259256 hashs with 120619948 bytes (14.47% of keys, avg size 465.25)
765522 strings with 70870968 bytes (42.73% of keys, avg size 92.58)
135652 sets with 69888816 bytes (07.57% of keys, avg size 515.21)
500641 zsets with 536634263 bytes (27.94% of keys, avg size 1071.89)

比 –bigkeys 好在哪:能看到 msg_wait_queue 这个 List 占了 289MB——光看元素数(20 万)感觉还好,但实际内存是全库最大的。

2.3 它们的共同局限

两个命令都只能找每种类型最大的那一个 Key。但实际排查需要知道:

  • 所有超过 10KB 的 Key(不只是最大的)
  • 某个前缀的 Key 总共占了多少内存(比如 user_cache_* 有 100 万个,单个不大但总量惊人)
  • 按类型 / 编码 / 过期时间分组统计
  • 这些它们做不到。必须导出 RDB 文件做离线全量分析。


    三、工具选型:连踩两个坑

    3.1 ❌ redis-rdb-tools(Python,不支持 RDB 12)

    这是最经典的 RDB 分析工具,但最后更新是 2020 年,不支持 Redis 7.4+ 的 RDB version 12:

    rdb -c memory -o ./dump_memory.csv ./dump.rdb

    # 报错
    Exception: ('verify_version', 'Invalid RDB version number 12')

    3.2 ❌ HDT3213/rdb(Go,同样不支持)

    换了一个 Go 实现的工具,还是不行:

    curl -L -o rdb https://github.com/HDT3213/rdb/releases/latest/download/rdb-darwin-arm64
    chmod +x rdb
    ./rdb -c memory -o ./dump_memory.csv ./dump.rdb

    # 报错
    error: unsupported opcode 244 in Redis RDB version 12

    3.3 ✅ redis-rdb-cli(Java,支持 RDB 12)

    第三个工具 redis-rdb-cli,Java 实现,维护活跃,终于能用:

    # 安装
    brew tap leonchen83/redis-rdb-cli
    brew install redis-rdb-cli

    # 导出全量 Key 内存数据
    rct -f mem -s ./dump.rdb -o ./dump_memory.csv

    # 只导出 Top 50 大 Key
    rct -f mem -s ./dump.rdb -o ./dump_memory.csv -l 50

    # 只导出大于 1MB 的 Key
    rct -f mem -s ./dump.rdb -o ./dump_memory.csv -b 1048576

    导出速度约 66.5MB/s,1.5GB 的 RDB 文件几十秒搞定。

    3.4 工具对比

    工具语言RDB 12最后更新推荐
    redis-rdb-tools Python 2020 老项目可用
    HDT3213/rdb Go 2024 低版本 RDB 可用
    redis-rdb-cli Java 活跃 推荐

    四、导出数据长什么样

    导出的 CSV 格式:

    database,type,key,size_in_bytes,encoding,num_elements,len_largest_element,expiry
    0,list,"yunxin_msg_queue","277.5MB",quicklist2,183498,"2.2kB",""
    0,sortedset,"auto_like_vip","81.1MB",skiplist,626255,"29 B",""
    0,sortedset,"last_time_zset_123456","79.8MB",skiplist,869100,"8 B","2026-03-09 12:57:42.981"

    列含义
    database Redis DB 编号
    type 数据类型:string / list / hash / set / sortedset
    key Key 名称
    size_in_bytes 占用大小(带单位,如 277.5MB、2.2kB)
    encoding 内部编码:quicklist2 / skiplist / hashtable 等
    num_elements 元素数量
    len_largest_element 最大元素大小
    expiry 过期时间(空 = 永不过期)

    ⚠️ size_in_bytes 列虽然叫 bytes,但输出的是人类可读格式(MB/kB),不是纯数字。后面用 SQL 分析时需要转换。


    五、导入数据库 + SQL 分析

    CSV 导出后,丢进数据库用 SQL 分析,比 Excel 灵活 100 倍。

    方案 A:ClickHouse(推荐,有 parseReadableSize 函数)

    建表:

    CREATE TABLE tmp.redis_memory (
    `database` UInt8 COMMENT 'Redis DB 编号',
    `type` String COMMENT '数据类型',
    `key` String COMMENT 'Redis Key',
    size_in_bytes String COMMENT '占用大小(带单位)',
    encoding String COMMENT '内部编码',
    num_elements UInt64 COMMENT '元素数量',
    len_largest_element String COMMENT '最大元素大小(带单位)',
    expiry Nullable(String) COMMENT '过期时间'
    ) ENGINE = MergeTree()
    ORDER BY tuple();

    导入:

    # 拷贝到容器
    docker cp ./dump_memory.csv clickhouse-server:/tmp/

    # 导入
    docker exec -i clickhouse-server clickhouse-client \\
    –user=admin –password='xxx' \\
    –query="INSERT INTO tmp.redis_memory FORMAT CSVWithNames" \\
    < /tmp/dump_memory.csv

    分析——ClickHouse 的 parseReadableSize() 函数可以直接把 277.5MB 转成字节数:

    — Top 50 大 Key
    SELECT
    `type`,
    `key`,
    size_in_bytes,
    parseReadableSize(size_in_bytes) AS bytes,
    formatReadableSize(parseReadableSize(size_in_bytes)) AS readable,
    num_elements,
    encoding
    FROM tmp.redis_memory
    ORDER BY bytes DESC
    LIMIT 50;

    方案 B:MySQL

    建表:

    CREATE TABLE redis_memory (
    id INT AUTO_INCREMENT PRIMARY KEY,
    `database` INT COMMENT 'Redis DB 编号',
    `type` VARCHAR(20) COMMENT '数据类型',
    `key` VARCHAR(500) COMMENT 'Redis Key',
    size_in_bytes VARCHAR(20) COMMENT '占用大小(带单位)',
    encoding VARCHAR(30) COMMENT '内部编码',
    num_elements BIGINT COMMENT '元素数量',
    len_largest_element VARCHAR(20) COMMENT '最大元素大小(带单位)',
    expiry VARCHAR(30) COMMENT '过期时间'
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    分析——MySQL 没有 parseReadableSize,需要 CASE WHEN 转换:

    SELECT `type`, `key`, size_in_bytes, num_elements,
    CASE
    WHEN size_in_bytes LIKE '%GB' THEN CAST(REPLACE(size_in_bytes, 'GB', '') AS DECIMAL(20,2)) * 1073741824
    WHEN size_in_bytes LIKE '%MB' THEN CAST(REPLACE(size_in_bytes, 'MB', '') AS DECIMAL(20,2)) * 1048576
    WHEN size_in_bytes LIKE '%kB' THEN CAST(REPLACE(size_in_bytes, 'kB', '') AS DECIMAL(20,2)) * 1024
    WHEN size_in_bytes LIKE '% B' THEN CAST(REPLACE(size_in_bytes, ' B', '') AS DECIMAL(20,2))
    END AS bytes
    FROM redis_memory
    ORDER BY bytes DESC
    LIMIT 50;


    六、实战:用 SQL 揪出隐藏大户

    6.1 直接找大 Key(单个 Key 大)

    — Value > 1MB 的 Key
    SELECT * FROM tmp.redis_memory
    WHERE parseReadableSize(size_in_bytes) > 1048576
    ORDER BY parseReadableSize(size_in_bytes) DESC;

    — 元素数 > 10000 的 Key
    SELECT * FROM tmp.redis_memory
    WHERE num_elements > 10000
    ORDER BY num_elements DESC;

    结果示例:

    typekeysize_in_bytesnum_elementsencoding
    list yunxin_msg_queue 277.5MB 183,498 quicklist2
    sortedset auto_like_vip 81.0MB 626,255 skiplist
    sortedset last_time_zset_123456 79.8MB 869,100 skiplist

    6.2 前缀分析法(找"蚂蚁搬家"型大户)

    有些 Key 单个不大(几 KB),但同一前缀有几十万甚至上亿个,加起来吃掉几个 GB——这种 –bigkeys 完全发现不了。

    第一步:按首字母聚合

    SELECT
    substr(`key`, 1, 1) AS initial,
    formatReadableSize(sum(parseReadableSize(size_in_bytes))) AS total_size,
    count() AS key_count
    FROM tmp.redis_memory
    GROUP BY initial
    ORDER BY sum(parseReadableSize(size_in_bytes)) DESC;

    initialtotal_sizekey_count
    a 3.28 GiB 1,234,567
    s 306.47 MiB 45,678
    y 264.64 MiB 12

    a 开头的 Key 占了 3.3GB!继续往下挖。

    第二步:缩小前缀范围

    SELECT
    substr(`key`, 1, 4) AS prefix,
    formatReadableSize(sum(parseReadableSize(size_in_bytes))) AS total_size,
    count() AS key_count
    FROM tmp.redis_memory
    WHERE `key` LIKE 'a%'
    GROUP BY prefix
    ORDER BY sum(parseReadableSize(size_in_bytes)) DESC
    LIMIT 10;

    prefixtotal_sizekey_count
    a2b_ 3.06 GiB 1,200,000
    acco 212.80 MiB 34,567

    真凶浮出水面:a2b_* 前缀的 Key 有 120 万个,总共占了 3GB。单个 Key 才几 KB,–bigkeys 根本看不见它。

    第三步:确认 Key 模式

    SELECT `key`, size_in_bytes, num_elements, expiry
    FROM tmp.redis_memory
    WHERE `key` LIKE 'a2b_%'
    LIMIT 10;

    一看就明白了:a2b_{userA_id}_{userB_id},是用户关系的缓存,每对用户一个 Key。大部分没有设过期时间,只进不出。


    七、找到大 Key 之后怎么处理

    场景方案
    永不过期的缓存堆积 加 TTL,设置合理过期时间
    单个 Key 过大(如 200MB 的 List) 拆分成多个小 Key(分片),或改用 Stream
    业务已不用的冷数据 直接删除(大 Key 用 UNLINK 异步删,别用 DEL)
    集合元素过多 定期清理过期元素,或拆分到多个 Key
    前缀类 Key 数量爆炸 评估是否需要缓存、能否合并、能否加 TTL

    删大 Key 用 UNLINK:Redis 4.0+ 支持,后台线程异步删除,不阻塞主线程。DEL 大 Key 会同步释放内存,可能阻塞几秒。


    八、总结

    步骤工具 / 命令
    快速看最大 Key redis-cli –bigkeys
    导出全量 Key 内存 rct -f mem -s ./dump.rdb -o ./dump.csv(redis-rdb-cli)
    导入分析 ClickHouse(推荐,有 parseReadableSize)或 MySQL
    找单个大 Key ORDER BY bytes DESC
    找前缀大户 GROUP BY substr(key, 1, N) 逐级下钻
    处理大 Key 加 TTL / 拆分 / UNLINK 删除

    三句话:

  • –bigkeys 只能看到冰山一角,全量分析靠 RDB 离线导出
  • Redis 7.4+ 的 RDB 12 格式,目前只有 redis-rdb-cli 能正常解析
  • 大 Key 不一定是单个大——120 万个小 Key 同一前缀加起来 3GB,才是最容易忽视的隐藏大户

  • 参考资料

    • redis-rdb-cli – GitHub
    • 阿里云 Redis 最佳实践
    • 京东云 – Redis 大 Key 问题解析
    赞(0)
    未经允许不得转载:171主机测评 » Redis 大 Key 排查实战:连踩两个工具的坑,最终用 SQL 揪出 3.3GB 的隐藏大户
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址