线上 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。但实际排查需要知道:

  1. 所有超过 10KB 的 Key(不只是最大的)
  2. 某个前缀的 Key 总共占了多少内存(比如 user_cache_* 有 100 万个,单个不大但总量惊人)
  3. 按类型 / 编码 / 过期时间分组统计

这些它们做不到。必须导出 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;

结果示例:

type key size_in_bytes num_elements encoding
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;
initial total_size key_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;
prefix total_size key_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 删除

三句话

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

参考资料

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐