Redis 大Key与热Key排查实战:从告警到根治

Redis 大Key 与热Key 是生产环境最常见的性能炸弹。一个几百 MB 的 Hash、一个被万级 QPS 疯抢的热点 Key,都可能在瞬间拖垮整个 Redis 实例,进而引发上游服务雪崩。本文从告警现象出发,拆解大Key与热Key 的判定标准、排查命令与根治方案,帮你把隐患挡在上线之前。

一、先认清敌人:什么是大Key与热Key

两者都叫“问题 Key”,但成因和破坏路径完全不同。大Key 指的是单个 Key 占用的内存或元素数量过大;热Key 指的是单个 Key 的访问频率(读/写 QPS)远高于其他 Key。一个 Key 可以既是大Key 又是热Key,危害会叠加。

数据类型大Key 阈值(经验值)说明
Stringvalue > 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 才能真正稳如磐石。

上一篇 gitleaks 实战:在 CI 中自动拦截密钥泄露