Redis 大Key 与热Key 是生产环境最常见的性能炸弹。一个几百 MB 的 Hash、一个被万级 QPS 疯抢的热点 Key,都可能在瞬间拖垮整个 Redis 实例,进而引发上游服务雪崩。本文从告警现象出发,拆解大Key与热Key 的判定标准、排查命令与根治方案,帮你把隐患挡在上线之前。
一、先认清敌人:什么是大Key与热Key
两者都叫“问题 Key”,但成因和破坏路径完全不同。大Key 指的是单个 Key 占用的内存或元素数量过大;热Key 指的是单个 Key 的访问频率(读/写 QPS)远高于其他 Key。一个 Key 可以既是大Key 又是热Key,危害会叠加。
| 数据类型 | 大Key 阈值(经验值) | 说明 |
|---|---|---|
| String | value > 10 KB(> 1 MB 算严重) | 单值过大,序列化与网络传输都慢 |
| Hash / Set / ZSet / List | 元素数 > 5000(> 1 万为严重) | 单 Key 持有过多元素,删除或遍历会阻塞 |
| 热Key | 单 Key QPS 占实例总 QPS 的 10%~20% | 流量极度倾斜,单点被打满 |
二、它们是怎么搞垮 Redis 的
大Key 的主要危害是阻塞主线程。Redis 是单线程处理命令,对大Key 执行 DEL、HGETALL、SMEMBERS 这类操作会长时间占用线程,期间其他请求全部排队,表现为“间歇性卡顿”。此外,大Key 在主从同步、AOF 重写、RDB 生成时会产生瞬时大流量,打满带宽或触发集群数据倾斜。
热Key 的危害则是单点过载。所有流量集中到一个分片,该分片的 CPU、带宽、连接数被占满,而集群其他节点却很闲。一旦这个节点撑不住,依赖它的上游服务就会集体超时。关于热Key 与缓存三大问题的区别,可回顾 Redis 缓存设计:穿透、击穿、雪崩与最佳实践。
三、如何发现:监控 + 命令双管齐下
3.1 官方内置扫描
Redis 自带 --bigkeys 和 --hotkeys 两个扫描开关,底层都基于 SCAN,对线上实例几乎无阻塞。
# 大Key 扫描(低阻塞,生产可用)
redis-cli -h 127.0.0.1 -p 6379 --bigkeys
# 指定数据库 + 扫描间隔,避免一次性打满 CPU
redis-cli -n 2 --bigkeys -i 0.01
# 热Key 扫描(需 maxmemory-policy 开启 LFU:allkeys-lfu / volatile-lfu)
redis-cli --hotkeys
# 查看某 Key 的访问频率(object freq,要求 LFU 策略)
redis-cli object freq user:10086:profile
3.2 自定义精准度量
内置扫描只能给出“最大”的几个 Key,若要按业务前缀全量排查,可结合 SCAN 与 MEMORY USAGE 自己遍历。
# 用 SCAll 遍历 + MEMORY USAGE 度量每个 Key 的真实内存占用
redis-cli --scan --pattern "order:*" | head -200 | \
while read k; do
size=$(redis-cli memory usage "$k")
echo "$k $size"
done | sort -k2 -n -r | head -20
3.3 把排查做成常态化监控
靠人工跑命令永远慢半拍。把 Redis Exporter 接入 Prometheus + Grafana 监控面板,对内存使用率、带宽、命中率设阈值告警,大Key 往往就是内存曲线异常跳涨的元凶。建议至少盯住四类面板:实例级内存与最大内存比、各分片 CPU 与带宽分布(识别倾斜)、Key 命中率与逐出数、以及命令耗时 P99。热Key 通常表现为“某分片 CPU/带宽一枝独秀”,而大Key 更多体现在内存与删除耗时上,两者配合排查效率最高。
# 单实例内存使用率超 80% 告警(大Key 常是元凶)
- alert: RedisMemoryHigh
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.8
for: 5m
labels: { severity: warning }
annotations:
summary: "Redis 内存使用率过高,疑似大Key"
四、大Key 根治方案
4.1 拆分与压缩
把“一个 100 万 field 的 Hash”拆成“100 个 1 万 field 的 Hash”,用固定分片数取模命名(如 user:action:{uid%100})。对 String 大Value,先做业务裁剪,必要时用 gzip/snappy 压缩后再存。
4.2 安全删除:别用 DEL
直接 DEL 一个超大 Key 会同步释放内存并长时间阻塞主线程。必须用 UNLINK(异步懒删除),或写 Lua 脚本分批删除。
# 删除大Key 用 UNLINK(异步,不阻塞主线程)
UNLINK order:2024:huge
-- 分批删除大 Hash,避免单次遍历卡死(每批删 500 个 field)
local cursor = 0
repeat
local res = redis.call('HSCAN', KEYS[1], cursor, 'COUNT', 500)
cursor = tonumber(res[1])
for _, f in ipairs(res[2]) do redis.call('HDEL', KEYS[1], f) end
until cursor == 0
redis.call('DEL', KEYS[1])
五、热Key 根治方案
5.1 本地缓存兜底
在应用进程内加一层本地缓存(Caffeine / Guava),热点数据先读本地,未命中再回源 Redis。这样能把 90% 以上的热Key 流量在进程内消化掉。更深入的 Redis 用法参见 Redis 分布式锁与高级数据结构实战。
5.2 多副本打散
给热点 Key 加随机后缀,把单点流量摊到多个副本上;写入时全量写,读取时随机选一个副本。
// 给热点 Key 加随机后缀,把单点流量摊到多个副本
int replicas = 10;
int idx = ThreadLocalRandom.current().nextInt(replicas);
String hotKey = "sku:stock:" + skuId + ":r" + idx;
String val = redis.get(hotKey);
if (val == null) {
val = loadFromDb(skuId); // 回源只打一次 DB
for (int i = 0; i < replicas; i++) { // 写时全量写,读时随机读
redis.set("sku:stock:" + skuId + ":r" + i, val, 30, SECONDS);
}
}
5.3 限流与读写分离
对热点接口在网关层做限流,并让读请求走从节点。当单 Key 流量超过阈值时,限流能防止故障扩散,避免像 连接池耗尽排查 那样从一处资源枯竭演变成系统性雪崩。限流阈值建议结合压测得出的单分片承载上限来设,而不是拍脑袋;同时把热Key 的本地缓存命中率纳入看板,命中率掉到阈值以下就触发告警。
更进一步,可以在 CI 里加一个静态扫描:用正则匹配代码库中“一次性写入全量集合”“未设置 TTL 的累计型 Key”等危险写法,在合并前就拦下来。把排查经验沉淀成规则,比事后救火成本低一个数量级。
六、预防:把坑挡在上线之前
排查是救火,预防才是正道。把容量评估、编码规范、压测和监控串成一条线,多数大Key/热Key 根本不会活到线上。
| 阶段 | 动作 | 工具 / 指标 |
|---|---|---|
| 设计 | 容量评估,禁止大 Value | 设计评审 |
| 编码 | 禁止一次性存全量集合 | Code Review |
| 测试 | 压测 + 数据预热 | 压测平台 |
| 运维 | 监控内存/带宽/命中率 | Prometheus |
| 应急 | 预案:UNLINK / 限流 / 打散 | 预案文档 |
七、一次真实复盘
某次大促前,监控告警显示 Redis 内存 5 分钟内从 40% 飙到 92%。用 --bigkeys 一查,发现一个累计了三年的“全站用户行为日志” Hash,单 Key 1.2 GB。由于直接 DEL 会阻塞,我们先挂了只读降级开关,再用 Lua 分批 UNLINK 花了约 8 分钟清理完,内存回落,接口 P99 从 1.8s 回到 40ms。事后我们把“禁止无 TTL 的累计型大 Key”写进了编码规范。
八、小结
大Key 与热Key 的本质都是“资源分布不均”:一个是空间不均,一个是时间不均。排查靠 SCAN + MEMORY USAGE + 监控三件套,根治靠拆分、压缩、异步删除、本地缓存和打散。把它们纳入常态化监控和上线规范,Redis 才能真正稳如磐石。




