什么是大 Key 问题,如何解决?

#中间件#面试八股

什么是大 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 安全删除。
  • 最有效的预防是在写入路径设上限,而不是等线上抖动后再治理。
笔记加载中…