什么是大 Key 问题,如何解决?
结论:大 Key 指**单个 value 体积过大(常见阈值 10 KB 以上)或集合元素过多(如 Hash/List/ZSet 成员数超 5000)**的 key。它不是"占内存多"这么简单,真正的危害是:单次操作耗时变长会阻塞单线程、网络包变大导致超时、删除与迁移变慢、集群分片不均。治理思路是"先发现、再拆分、最后改访问方式"。
一、危害链路
大 Key(如 200 MB 的 Hash)
│
├─ HGETALL / SMEMBERS → 单次响应几十 MB → 网络拥塞 + 客户端 OOM
├─ DEL → 同步释放内存,主线程卡顿数百毫秒
├─ 过期/淘汰 → 同样同步释放,形成延迟毛刺
├─ 主从同步/迁移 slot → 单 key 无法拆分,拖慢全量同步
├─ RDB fork COW → 大页复制,内存峰值上升
└─ Cluster 分片 → 单个槽数据倾斜,热点集中在一个节点
最关键的一条:Redis 单线程处理命令,一个大 Key 的慢操作会让它后面排队的几十万请求一起等待,表现为"整实例抖动"而不是"某个接口慢"。
二、判定标准与发现手段
| 类型 | 体积阈值 | 元素数阈值 | 常见来源 |
|---|---|---|---|
| String | > 10 KB | — | 整篇 HTML、序列化大对象、图片 base64 |
| Hash | > 10 KB | > 5000 字段 | 用户属性全量塞一个 key、购物车不拆 |
| List | > 10 KB | > 5000 元素 | 无上限的队列、消息堆积 |
| Set | > 10 KB | > 5000 元素 | 全站用户 ID、标签全集 |
| ZSet | > 10 KB | > 5000 元素 | 全量榜单、延时任务不清理 |
# 1) 官方巡检:内部就是 SCAN + TYPE + 抽样估算
redis-cli --bigkeys
redis-cli --memkeys --memkeys-samples 0 # 更精确地按内存排序
# 2) 精确看某个 key 的内存与元素数
redis-cli MEMORY USAGE user:1001:profile SAMPLES 0
redis-cli STRLEN big:string
redis-cli HLEN big:hash
redis-cli LLEN big:list
redis-cli ZCARD big:zset
# 3) 慢查询里反查大 Key
redis-cli SLOWLOG GET 20
# 4) 离线分析 RDB(不打扰线上)
rdb --command memory dump.rdb | sort -k2 -nr | head -20
三、治理方案
- 拆分:按维度打散。如
user:{id}:profile拆成user:{id}:base、user:{id}:ext;按时间拆feed:20261103;按分片拆cart:{id%16}。 - 压缩:序列化从 JSON 换 Protobuf/MessagePack,文本先 gzip;图片只存 URL。
- 改访问方式:用
HSCAN/SSCAN代替HGETALL/SMEMBERS,用HMGET只取需要的字段,用ZRANGE分页代替全量取。 - 异步删除:
UNLINK代替DEL,FLUSHALL ASYNC代替FLUSHALL;4.0+ 的 lazy free 把释放放到后台线程。 - 设上限:List/Stream 用
LTRIM/MAXLEN控制长度,ZSet 定期清理过期成员。
// 分页读取替代 HGETALL:每次 200 个字段,避免一次性拉全量
Map<Object, Object> partial = new HashMap<>();
try (Cursor<Map.Entry<Object, Object>> c =
redis.opsForHash().scan("user:1001:profile",
ScanOptions.scanOptions().count(200).build())) {
while (c.hasNext()) {
Map.Entry<Object, Object> e = c.next();
partial.put(e.getKey(), e.getValue());
}
}
四、删除大 Key 的安全姿势
# 危险:DEL 会同步释放,200MB 的 hash 可能卡住主线程数百毫秒
DEL big:hash
# 安全:UNLINK 把释放交给后台线程,主线程只做摘链
UNLINK big:hash
# 更稳:先分批删元素,再删 key(避免后台线程也被单个大对象拖慢)
# 循环 HSCAN + HDEL,每批 500 个,直到 HLEN 为 0,最后 DEL
五、常见追问
- 问:大 Key 和热 Key 是一回事吗?答:不是。大 Key 看"体积/元素数",热 Key 看"访问频次"。二者会叠加成最坏情况:又大又热,单次操作既慢又频繁。
- 问:为什么
MEMORY USAGE有时候不准?答:它按采样估算,SAMPLES 0表示全量精确计算但更慢;对集合类 key,估算误差可能较大。 - 问:拆分后如何做聚合查询?答:把聚合放到写入侧(如定时任务汇总)或读侧应用层合并;Redis 不做跨 key 聚合,别指望
MGET之外的联合操作。 - 问:集群模式下大 Key 为什么更危险?答:整个 key 只能落在一个槽、一个节点,无法拆分到多节点,直接造成数据倾斜与单节点内存打满。
- 问:如何在上线前拦住大 Key?答:在 DAO 层做 value 大小校验(超阈值打日志/告警),对集合写入做长度上限检查,把"写入即校验"作为规范。
- 问:
--bigkeys会阻塞吗?答:它用 SCAN 遍历,本身不阻塞,但会对每个 key 执行TYPE与抽样统计,量大时仍建议在从库或低峰执行。
小结
- 大 Key 的本质是"单次操作的粒度太大",危害集中在阻塞主线程与拖慢迁移。
- 治理顺序:巡检发现 → 拆分与压缩 → 改批量访问为分页访问 → 用 UNLINK 安全删除。
- 最有效的预防是在写入路径设上限,而不是等线上抖动后再治理。