跳到正文

大 Key 问题 ​

一句话:大 Key 是单个 key 的内存或元素数远超正常量级;危害不在「存得多」,而在网络传输、主线程阻塞和集群内存倾斜。治本是拆分,删除用 UNLINK。

适用版本:Redis 7.x / 8.x。UNLINK 自 4.0 起提供。

为什么需要 ​

Redis 命令在主线程执行,一个 key 的元素越多,遍历、序列化、释放占用的时间越长。单机看起来只是「这条命令慢一点」,在集群里还会变成「这个节点内存和带宽被打满,其他节点闲着」。大 Key 没有官方硬标准,但它是容量规划和故障排查里绕不开的一类问题。

核心机制 ​

判定:经验阈值 ​

类型经验阈值说明
Stringvalue > 10 KB 关注,> 1 MB 危险网络传输和序列化成本随体积线性增长
Hash / List / Set / ZSet元素数 > 5000遍历、删除、迁移都变慢
通用MEMORY USAGE > 几 MB跨类型参考

这些是行业经验值,不是 Redis 的硬限制。一个 1 KB 的 key 如果是热点,也会造成问题,但那属于热点 Key,见 11-热点Key问题。

四个危害 ​

  1. 内存倾斜:Cluster 按 slot 分片,一个大 key 全落在一个节点,该节点先 OOM/淘汰,其他节点还有余量。
  2. 网络阻塞:一次 HGETALL 返回几 MB,占满出方向带宽,同节点其他请求排队。
  3. 慢查询:HGETALL、LRANGE 0 -1、SMEMBERS 是 O(N),遍历 + RESP 序列化 + 写 socket 都在主线程完成。
  4. 删除高危:DEL 同步释放内存,删几百万元素可能卡住主线程数秒;UNLINK 只断开引用,把释放交给后台线程。

DEL 在主线程遍历整个数据结构并逐个 free();元素越多,free() 次数越多,耗时越不可控。UNLINK 在主线程只做 O(1) 的引用摘除,真正的内存回收由 BIO 线程完成。4.0 之前没有 UNLINK,只能用 SCAN + 分批 DEL。

最小上手 ​

bash
# 找出各类型最大的 key(内部用 SCAN,但仍会给实例带来压力)
redis-cli --bigkeys

# 按内存占用找 top key(4.0+,内部用 MEMORY USAGE)
redis-cli --memkeys

# 精确看单个 key
redis-cli MEMORY USAGE user:profile
redis-cli MEMORY USAGE user:profile SAMPLES 0   # 0 表示全量计算,大 key 上很慢

# 安全删除
redis-cli UNLINK user:profile

批量巡检时用 SCAN,不要用 KEYS:

bash
redis-cli --scan --pattern 'user:*' | head -20 | while read k; do
  echo "$k $(redis-cli MEMORY USAGE "$k")"
done

生产动作 ​

  • 发现工具都有局限:--bigkeys 按元素数/字节数排序,String 报 bytes、集合报 items,不直接反映内存;--memkeys 依赖 MEMORY USAGE,对嵌套结构默认只采样 5 个元素估算。精确值用 SAMPLES 0,但会遍历整个 key。
  • 执行时机:--bigkeys/--memkeys 会全库 SCAN,在低峰执行,或从从节点/离线副本上跑。
  • 写入前拦截:应用层计算 value 大小或集合长度,超过阈值拒绝写入或告警,比事后清理便宜。
  • 拆分:Hash 按 field 哈希分片,List 按时间/序号分段,Set 按成员取模分片,ZSet 按 score 范围分段;查询逻辑要跟着改。
  • 压缩:大 String 用 Snappy/LZ4/Zstd 压缩,适合访问频率低的冷数据;热点数据压缩后解压开销可能超过收益。
  • 安全删除:优先 UNLINK;开 lazyfree-lazy-expire yes、lazyfree-lazy-eviction yes、lazyfree-lazy-server-del yes,但客户端显式 DEL 仍是同步的。
  • 监控:SLOWLOG GET、INFO commandstats、集群各节点内存/CPU 对比;迁移大 Key 时关注 MIGRATE 耗时。

边界 ​

  • 阈值是经验值:不同业务对 10 KB / 5000 元素的容忍度不同,关键是看延迟和内存曲线,不是背数字。
  • --bigkeys 会漏:它只看元素数/字节数,不反映实际内存;一个含 1000 个 10 KB 字段的 Hash 可能元素数不超标,但内存很大。
  • MEMORY USAGE 默认是估算:SAMPLES 默认 5,嵌套结构越多误差越大;SAMPLES 0 精确但慢。
  • 拆分有代价:多 key 后跨分片查询、事务、聚合都要重写,可能引入一致性问题。
  • UNLINK 不是免费:后台释放任务堆积会占用 CPU 和内存,需监控延迟释放队列。
  • 大 Key 迁移慢:MIGRATE 是 DUMP + RESTORE,大 key 会阻塞源节点和目标节点,扩缩容要避开。

讲清楚 ​

大 Key 的问题不是「占内存」这么简单,而是它把 O(N) 的操作放到了单线程的主线程上:一次 HGETALL 要遍历并序列化几 MB,一次 DEL 要释放几十万个对象,整个实例都跟着停。集群里还会因为一个 slot 落在大 key 上,造成单节点内存和带宽倾斜。

处理顺序是:先用 --bigkeys/--memkeys/MEMORY USAGE 找到它,能拆就拆,不能拆就压缩或改类型;删除一定用 UNLINK。工具的结果都是估算或按元素数排序,最终要结合延迟和内存曲线判断。

把一门技术讲透到既懂也会用