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;
结果示例:
| 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 删除 |
三句话:
--bigkeys只能看到冰山一角,全量分析靠 RDB 离线导出- Redis 7.4+ 的 RDB 12 格式,目前只有 redis-rdb-cli 能正常解析
- 大 Key 不一定是单个大——120 万个小 Key 同一前缀加起来 3GB,才是最容易忽视的隐藏大户
参考资料
更多推荐




所有评论(0)