大 key 与热 key 治理
大 key 决定内存与阻塞风险,热 key 决定集群是否倾斜。两者经常同时出现:一个被高频访问的大 hash,既拖慢单线程执行,又把某一个分片的 CPU 打满。本章给出判定标准、发现手段、拆分前后对比,以及在线的安全迁移步骤。
判定标准
| 类型 | 关注阈值 | 严重阈值 | 主要风险 |
|---|---|---|---|
| String | value 大于 10KB | 大于 1MB | 单次读写耗时高、网络带宽被占 |
| Hash / List / Set / ZSet | 元素数大于 5000 | 大于 10000 或总内存大于 1MB | HGETALL/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 打满,其余分片闲置,扩容也无法分担。
- 分片打满后延迟上升,客户端超时重试,重试流量进一步压在同一分片上,形成重试风暴。
- 主从架构下该分片的所有从库同步承受同样压力,读写分离救不了倾斜。
- 热点数据一旦过期或被淘汰,瞬时回源会把数据库打出一个尖峰。
治理:拆分与分散的前后对比
| 场景 | 拆分前 | 拆分后 | 代价 |
|---|---|---|---|
| 用户属性全量存在一个 hash | HGETALL user:1001,100 万字段 | 按维度拆成 user:1001:base、user: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 两级缓存 | 有一致性延迟,需要失效广播 |
| 写多读少的计数类 | 全部写同一个 key | key 加随机后缀分散到多分片,异步聚合 | 读要合并 N 份,聚合逻辑变复杂 |
取舍观点:加随机后缀只适合“写多读少、能容忍最终聚合”的计数场景。读多写少的详情类绝对不要用随机后缀——那会把一次读变成 N 次读,热点没解决反而更慢,这类场景的正确解法是本地缓存。
在线拆分的安全步骤
- 双写:新写入同时落到新结构和旧结构,读仍走旧结构。
- 分批迁移:用
HSCAN/SSCAN按 100 ~ 500 个元素一批迁移,批间加几毫秒间隔,避免自己制造阻塞。 - 校验:比对元素总数与抽样内容,确认无遗漏。
- 切读:灰度放量到新结构,观察命中率与延迟一周。
- 删旧:用
UNLINK删除旧 key,不要用DEL直接删大对象。
大 key 的常见来源(设计阶段就该拦住)
| 来源 | 典型形态 | 设计阶段的对策 |
|---|---|---|
| 把数据库整行塞进一个 hash | 用户档案、商品详情的全字段 | 只缓存热点字段,其余按需回源 |
| 把集合当存储用 | 一个 key 存一年的行为记录 | 按天或按月分 key,并设置 TTL |
| 无界集合只加不删 | 评论列表、消息列表 | 用 LTRIM、ZREMRANGEBYRANK 定期裁剪 |
| 超长 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 |
预防
- 代码评审硬性禁止无界命令:
KEYS、HGETALL、SMEMBERS、LRANGE key 0 -1、ZRANGE key 0 -1。 - key 设计规范写死上限:单 key 元素数、单 value 大小、集合类数据的分片规则。
- 每周执行一次
--bigkeys/--memkeys巡检,报告归档,越线 key 建工单跟进。 - 热点可预测的场景(秒杀、大促)提前做本地缓存与静态化,而不是等故障发生再拆。
小结:大 key 用元素数与内存占用两条线判定,靠 --bigkeys、MEMORY USAGE 与 RDB 离线分析发现;热 key 靠 LFU 的 --hotkeys 与命令统计发现,危害是分片倾斜与重试放大;治理按“按维度拆分、按需取值、本地缓存兜底”三招走,随机后缀只留给能容忍聚合延迟的写密集场景。