Redis 大 key 与热 key 是线上缓存性能塌方的两大隐形杀手:前者悄悄撑爆内存、让删除操作阻塞整条主线程,后者把流量集中压到单节点,造成集群层面「一核有难、八核围观」。本文基于真实排雷经验,手把手教你用 redis-cli、MEMORY 命令与采样脚本定位这两类问题,并给出可落地的根治方案。
一、为什么大 key 和热 key 是缓存塌方的元凶
很多团队在学完 Redis 缓存三大问题(穿透、击穿、雪崩)的防护 后,以为缓存就稳了,结果大促前夜被一个 80MB 的 Hash 打挂。原因很简单:大 key 和热 key 不走「有没有命中」那套逻辑,它们直接攻击 Redis 的单线程模型——内存占用、网络带宽、命令耗时全部被单一 key 绑架。
深入理解单线程模型很关键:Redis 处理命令是串行的,一个 80MB 的 Hash 做 HGETALL 或 DEL,会在这几毫秒到几百毫秒内独占主线程,期间所有其他命令排队等待——这正是「大 key 让人感觉整站变慢」的底层原因。热 key 同理,海量 GET 打在同一分片,CPU 被单核吃满,集群其余节点却在围观。两者都源于「热点集中」,只是一个在体积、一个在频率。
二、大 key:如何不动声色拖垮集群
2.1 大 key 的典型形态
常见形态有四类:一个塞了几十万成员的 Hash/Set/ZSet;一条长度过万的 List;一个好几 MB 的 String(比如把整个页面 HTML 塞进一个 key);以及用 JSON 字符串当「大杂烩」容器。它们共同点是删除或序列化时极慢,而 Redis 删除大 key 默认是同步的,会阻塞主线程。
2.2 用 redis-cli –bigkeys 快速扫描
最省事的第一步,是让 Redis 自己在后台采样找出各类数据的最大 key:
# 被动采样,不会真遍历全库,对线上相对安全
redis-cli -h 10.0.0.12 -p 6379 --bigkeys
# 输出示例
# [00.00%] Biggest string found so far 'page:home' with 12 bytes
# [100.00%] Biggest hash found so far 'user:feed:8872' with 412330 fields
# -------- summary -------
# Biggest hash found 'user:feed:8872' has 412330 fields
它只报告「每种类型最大的那个」,精度有限,但胜在零成本、能立刻暴露明显的问题 key。
2.3 精准定位:MEMORY USAGE 与 DEBUG OBJECT
拿到嫌疑 key 后,用 MEMORY USAGE 测真实字节数(O(1),线上可用),用 DEBUG OBJECT 看序列化长度与剩余 TTL:
redis-cli -h 10.0.0.12 -p 6379 MEMORY USAGE user:feed:8872
(integer) 88421345 # 约 84MB,典型大 key
redis-cli -h 10.0.0.12 -p 6379 DEBUG OBJECT user:feed:8872
# Value at:0 refcount:1 encoding:hashtable serializedlength:812233 lru:14320118 lru_seconds_idle:182
2.4 生产安全的全量普查脚本
–bigkeys 只给「最大的一个」,要全量捞出所有超标 key,用 SCAN 分批游标 + MEMORY USAGE,避免 KEYS 阻塞主线程:
import redis
r = redis.Redis(host="10.0.0.12", port=6379, decode_responses=True)
BIG_THRESHOLD = 10240 # 超过 10KB 记为大 key
cursor = 0
while True:
cursor, keys = r.scan(cursor, count=500)
for k in keys:
size = r.memory_usage(k) # O(1),对线上安全
if size and size > BIG_THRESHOLD:
print(f"BIGKEY {k} {size}B type={r.type(k)}")
if cursor == 0:
break
三、热 key:被忽视的单点瓶颈
3.1 热 key 的三种信号
热 key 在监控上往往表现为:个别节点的 CPU 和带宽明显高于其他分片;INFO commandstats 里某条命令(如 GET order:detail:1)调用量断层领先;或者客户端出现「集群很闲、单请求却很慢」的诡异延迟。它和 连接池耗尽、端口耗尽这类资源耗尽故障 不同,瓶颈不在连接,而在单 key 的访问热点。
3.2 客户端埋点 + 代理层统计
Redis 7.4+ 自带 --hotkeys 选项,由 LFU 计数器直接给出热点 key;老版本则靠代理层(Codis/Twemproxy/集群代理)访问日志聚合,或业务侧用滑动窗口统计:
# Redis 7.4+ 直接输出热 key(基于 LFU)
redis-cli -h 10.0.0.12 -p 6379 --hotkeys
# 代理层访问日志聚合(示例:统计每秒 TOP 热 key)
redis-cli info commandstats | grep -i "cmdstat_get"
四、根治方案对照表
| 诊断手段 | 适用场景 | 是否阻塞 | 精度 |
| redis-cli –bigkeys | 快速总量扫描 | 被动采样,较安全 | 低(仅每种最大) |
| MEMORY USAGE | 单 key 精准体积 | 否(O(1)) | 高 |
| SCAN + memory_usage 脚本 | 全量普查大 key | 否(分批游标) | 高 |
| 代理层 / –hotkeys | 热 key 识别 | 否 | 中 |
4.1 大 key 的拆分与异步删除
根治思路是「拆」和「缓」:把大 Hash 按字段哈希成多个子 key;大 List 改用分页或分片;大 String 压缩或移出 Redis。删除时绝不用 DEL,改用 UNLINK(异步惰性删除,Redis 4.0+),把阻塞从主线程挪到后台线程:
# 异步删除,不阻塞主线程
redis-cli UNLINK user:feed:8872
4.2 热 key 的打散与就近缓存
热 key 靠「分散」解决:对 key 做固定分片(如 16 副本轮询),把单点压力摊到多个分片;或在客户端加一层本地缓存(Caffeine/Guava),把极端热点挡在 Redis 之前。合理设计分布式锁时也要避开热 key,参考 Redis 分布式锁与高级数据结构实战 的锁粒度建议。
// 热 key 本地缓存 + 多副本打散(Java 侧)
LoadingCache<String, String> local = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.build(k -> redis.get(k));
// key 分片,把单点压力摊到 16 个分片
String shardKey = "order:detail:" + (hash(userId) % 16) + ":" + userId;
4.3 容量规划:给大 key 留足余量
根治之后还要防复发:给 Redis 实例预留至少 30% 内存余量,对写入频繁的大集合设置成员数上限与 TTL,必要时把冷数据下沉到持久化层(可结合 PostgreSQL 慢查询优化与索引调优 的思路做冷热分离)。容量规划与监控双管齐下,大 key 才不会再偷偷长回来。
五、真实案例:一次大促前的缓存排雷
某次大促压测,监控显示其中一个分片 CPU 长期 95%,其余分片不到 20%。按本文流程:先用 –bigkeys 捞出 user:feed 系列大 Hash(单 key 84MB),再用 –hotkeys 发现首页推荐位 feed:top 每秒 12 万次读。处理动作:把 84MB 的 Hash 按用户 ID 取模拆成 64 个子 key,并用 UNLINK 替换 DEL;feed:top 加 Caffeine 本地缓存(30 秒过期)+ 16 副本分片。整改后该分片 CPU 降到 35%,P99 延迟从 180ms 落到 9ms,大促当天零缓存故障。
六、把排雷做成日常:监控与复盘
排雷不是一次性任务。建议把 MEMORY USAGE 普查接入定时任务(低峰期跑),把热 key 统计接进监控大盘,对超过阈值的 key 自动告警。新上线前在预发环境跑一遍 –bigkeys,能从源头拦住大部分大 key。缓存稳了,上层建筑才稳——这也是我们在缓存三大问题防护之外,必须补上的「大 key / 热 key」这一课。
七、上线前排雷 checklist
把排雷沉淀成可执行的检查项,比事后救火省心得多。每次发版前过一遍这四条:
- 大 key 预警:预发环境跑 –bigkeys,单 key 体积超 10KB 即预警,超 1MB 必须拆分。
- 热 key 打散:监控大盘接入 LFU 热 key 统计,单 key QPS 超过分片均值 5 倍即做分片或本地缓存。
- 删除规范:禁止 DEL 大 key,统一改用 UNLINK 异步删除,避免主线程抖动拖垮整实例。
- 容量水位:实例预留 30% 内存,大集合设成员上限与 TTL,冷数据下沉持久层做冷热分离。




