大 key 与热 key 治理

大 key 决定内存与阻塞风险,热 key 决定集群是否倾斜。两者经常同时出现:一个被高频访问的大 hash,既拖慢单线程执行,又把某一个分片的 CPU 打满。本章给出判定标准、发现手段、拆分前后对比,以及在线的安全迁移步骤。

判定标准

类型关注阈值严重阈值主要风险
Stringvalue 大于 10KB大于 1MB单次读写耗时高、网络带宽被占
Hash / List / Set / ZSet元素数大于 5000大于 10000 或总内存大于 1MBHGETALL/SMEMBERS 阻塞、DEL 卡顿
热 key 访问量单 key 超过实例总 QPS 的 10%单 key 打满单个分片分片倾斜、单点瓶颈、重试放大

阈值不是标准答案,要按实例规格、QPS 与延迟目标自己定义,并写进团队的 key 设计规范,否则每次评审都要重新吵一遍。

发现大 key

# 1. 扫描全库找每种类型最大的 key(内部走 SCAN,低峰期执行,-i 加间隔降低压力)
redis-cli --bigkeys -i 0.1
# 2. 按内存占用而不是元素数量扫描(Redis 4+,更贴近真实开销)
redis-cli --memkeys
# 3. 单个 key 的真实内存占用;SAMPLES 0 表示精确计算,在大 key 上会很慢
redis-cli memory usage user:profile:1001
redis-cli memory usage user:profile:1001 SAMPLES 0
# 4. 离线分析 RDB 文件,对线上零影响(需安装 rdb-tools)
rdb -c memory /data/dump.rdb -f /tmp/redis_memory.csv
sort -t, -k4 -nr /tmp/redis_memory.csv | head -20
# 5. 按前缀统计 key 数量,判断是否“海量小 key”而非“单个大 key”
redis-cli --scan --pattern 'user:profile:*' | wc -l

--bigkeys 的局限必须说清楚:它只报告每种类型的第一名,按元素数量或字符串长度排序,不统计总内存,也不告诉你有多少个 key 越线。1000 万个元素数 900 的小 hash 在它眼里毫无异常,但整体内存可能已经吃满实例。

发现热 key

# 前提:淘汰策略必须是 LFU 系列,否则 --hotkeys 会直接报错
redis-cli config get maxmemory-policy     # allkeys-lfu 或 volatile-lfu
redis-cli --hotkeys
# 按命令维度看谁在吃 CPU;快照是累计值,需要两次取差值
redis-cli info commandstats | grep -E 'cmdstat_(get|hgetall|zrange|incr)'

MONITOR 采样可以看实时命令分布,但生产环境慎用:它会把每条命令复制一份推给监控客户端,高 QPS 下自身就是新瓶颈,还可能触发输出缓冲超限被强断,甚至拖慢实例本身。只在极低峰、秒级窗口使用:

timeout 5 redis-cli monitor > /tmp/mon.txt
awk '{print $4}' /tmp/mon.txt | sort | uniq -c | sort -rn | head -20

热 key 的危害

  • Cluster 按 key 做哈希分片,一个热 key 只落在一个分片上:该分片 CPU 打满,其余分片闲置,扩容也无法分担。
  • 分片打满后延迟上升,客户端超时重试,重试流量进一步压在同一分片上,形成重试风暴。
  • 主从架构下该分片的所有从库同步承受同样压力,读写分离救不了倾斜。
  • 热点数据一旦过期或被淘汰,瞬时回源会把数据库打出一个尖峰。

治理:拆分与分散的前后对比

场景拆分前拆分后代价
用户属性全量存在一个 hashHGETALL user:1001,100 万字段按维度拆成 user:1001:baseuser:1001:stat,只用 HMGET 取需要的字段需要整理字段归属,读取要带字段清单
大 List 当队列单 key 100 万元素,LRANGE 0 -1按分片拆成 queue:{id}:{0..N},或用 Stream 消费组消费端要遍历多个分片,顺序语义变弱
大 ZSet 做排行ZRANGE 0 -1 取全量按时间维度切 rank:2024-06,只取 ZREVRANGE 0 99跨周期查询要合并多个 key
全局计数器单 key 每秒数万次 INCR拆成 16 个子 key counter:{0..15},读取时求和,或本地聚合后批量落读取要聚合,数值非实时精确
热点商品详情单 key 高频读,QPS 集中在单分片本地缓存(Caffeine / Go 本地缓存)+ Redis 两级缓存有一致性延迟,需要失效广播
写多读少的计数类全部写同一个 keykey 加随机后缀分散到多分片,异步聚合读要合并 N 份,聚合逻辑变复杂

取舍观点:加随机后缀只适合“写多读少、能容忍最终聚合”的计数场景。读多写少的详情类绝对不要用随机后缀——那会把一次读变成 N 次读,热点没解决反而更慢,这类场景的正确解法是本地缓存。

在线拆分的安全步骤

  1. 双写:新写入同时落到新结构和旧结构,读仍走旧结构。
  2. 分批迁移:用 HSCAN/SSCAN 按 100 ~ 500 个元素一批迁移,批间加几毫秒间隔,避免自己制造阻塞。
  3. 校验:比对元素总数与抽样内容,确认无遗漏。
  4. 切读:灰度放量到新结构,观察命中率与延迟一周。
  5. 删旧:用 UNLINK 删除旧 key,不要用 DEL 直接删大对象。

大 key 的常见来源(设计阶段就该拦住)

来源典型形态设计阶段的对策
把数据库整行塞进一个 hash用户档案、商品详情的全字段只缓存热点字段,其余按需回源
把集合当存储用一个 key 存一年的行为记录按天或按月分 key,并设置 TTL
无界集合只加不删评论列表、消息列表LTRIMZREMRANGEBYRANK 定期裁剪
超长 JSON 字符串单次写入 5MB 的 value拆成多个字段,或压缩后分片存储
定时任务全量重建每天重建同一个大 hash改成增量更新加分批写入

巡检与报告口径

# 低峰期生成清单并归档,与上周对比看是否有新增
redis-cli --bigkeys -i 0.1 > /tmp/bigkeys_$(date +%F).txt 2>&1
redis-cli --memkeys > /tmp/memkeys_$(date +%F).txt 2>&1
grep -E 'Sampled|biggest' /tmp/bigkeys_$(date +%F).txt

巡检报告至少写清四项:key 名、类型、元素数或字节数、负责人。连续两周出现在榜单上的 key 必须开单处理,否则每周巡检就只是表演。

常见误区

误区后果正确做法
只看 --bigkeys 榜单海量小 key 悄悄撑满内存,无人发现同时看 key 总数与内存分布
DEL 删除大对象单线程被阻塞几百毫秒UNLINK 异步删除
KEYS 找候选 key生产实例直接被卡住SCAN 或离线分析 RDB
热 key 只靠集群扩容倾斜没变,钱花了问题还在本地缓存或拆分 key

预防

  • 代码评审硬性禁止无界命令:KEYSHGETALLSMEMBERSLRANGE key 0 -1ZRANGE key 0 -1
  • key 设计规范写死上限:单 key 元素数、单 value 大小、集合类数据的分片规则。
  • 每周执行一次 --bigkeys/--memkeys 巡检,报告归档,越线 key 建工单跟进。
  • 热点可预测的场景(秒杀、大促)提前做本地缓存与静态化,而不是等故障发生再拆。

小结:大 key 用元素数与内存占用两条线判定,靠 --bigkeysMEMORY USAGE 与 RDB 离线分析发现;热 key 靠 LFU 的 --hotkeys 与命令统计发现,危害是分片倾斜与重试放大;治理按“按维度拆分、按需取值、本地缓存兜底”三招走,随机后缀只留给能容忍聚合延迟的写密集场景。

笔记加载中…