在生产 Redis 中,两个最隐蔽又最致命的敌人是大 Key(Big Key)与热 Key(Hot Key)。大 Key 会在不知不觉中拖垮单线程事件循环、撑爆内存、拉满网络带宽;热 Key 则会让某一条数据被百万并发同时访问,导致单个分片过载、缓存被击穿。
本文从界定与危害出发,讲解大 Key 的发现工具(redis-cli --bigkeys、SCAN + MEMORY USAGE)、拆分方案(Hash 分片/压缩)、热 Key 的探测(--hotkeys/Proxy 统计)与缓解手段(本地缓存/分片/多副本),最后给出监控告警与完整治理流程。
一、大 Key 与热 Key 的界定与危害
1.1 什么是大 Key 与热 Key
| 类型 | 界定标准 | 典型例子 |
|---|---|---|
| 大 Key(Big Key) | 单 Key 的 value 过大或元素过多 | String > 1MB、Hash/List/Set/ZSet 元素 > 1 万或体积 > 10MB |
| 热 Key(Hot Key) | 单 Key 访问频率远超平均水平 | 某商品 ID、直播间、用户,QPS 占全实例 30%+ |
1.2 大 Key 的四类危害
- 阻塞主线程:
DEL、HGETALL、LRANGE 0 -1、SUNION等 O(N) 命令会长时间阻塞单线程事件循环,导致全实例卡顿 - 内存不均匀:Cluster 集群中大 Key 使单个分片内存暴涨,节点间水位悬殊
- 网络阻塞:一次
GET一个 50MB 的 String,就传输 50MB 数据,轻易打满网卡 - 删除与淘汰惩罚:大 Key 过期后的惰性删除、LRU 淘汰都会产生 O(N) 开销
Redis 是单线程处理命令的,任何一个 O(N) 命令都在抢占所有用户的时间片。生产事故中最经典的"Redis 突然变慢",往往就是某次大 Key 操作引起的。
1.3 热 Key 的危害
热 Key 的危害集中在单点放大:无论请求路由到集群哪个节点,最终都汇聚到持有该 Key 的那一个分片。热点流量打满该分片的 CPU 和网络,其他分片闲置,整体吞吐上不去;热 Key 一旦过期或淘汰,瞬间的缓存穿透会让数据库承受百万 QPS 击穿流量。
1.4 治理闭环
发现(Detection) -> 定位(Location) -> 治理(Mitigation) -> 验证(Verify) -> 监控(Monitor)
--bigkeys MEMORY USAGE Hash分片/压缩/ 压测/流量回放 告警阈值+
--memkeys OBJECT FREQ 本地缓存/多副本 命中率对比 定期巡检
--hotkeys DEBUG OBJECT 分片key改造 回归
二、大 Key 的发现:扫描工具与命令
2.1 redis-cli –bigkeys:快速摸底
redis-cli --bigkeys 以 SCAN 方式遍历全库,统计每种数据结构中体积最大的 Key。
# 扫描全库大 Key
redis-cli --bigkeys
# Biggest string found so far '"order:20260927:000001"' with 52428800 bytes
# Biggest list found so far '"queue:pay"' with 500000 items
# 控制扫描节奏,降低对实例的影响
redis-cli --bigkeys -i 0.1 --scan-count 100
--bigkeys只统计"每种类型里最大的一个",看不到所有大 Key,适合快速摸底。它对 Hash 只显示字段数而非真实体积,无法替代精确排查。
2.2 精确排查:SCAN + MEMORY USAGE
用 SCAN 游标遍历 + MEMORY USAGE 度量真实内存占用:
# 遍历 user:* 前缀,输出内存占用超过 10MB 的 Key
redis-cli --scan --pattern "user:*" --count 1000 | \
while read k; do
size=$(redis-cli MEMORY USAGE "$k" SAMPLES 10)
if [ -n "$size" ] && [ "$size" -gt 10485760 ]; then
echo "$size $k"
fi
done | sort -rn | head -20
2.3 单 Key 体检命令
| 命令 | 用途 | 注意 |
|---|---|---|
MEMORY USAGE key | 精确内存占用 | 可带 SAMPLES n 抽样 |
DEBUG OBJECT key | serializedlength、lru 等内部字段 | 危险命令,仅排查用 |
OBJECT ENCODING key | 编码类型(raw/listpack) | 辅助判断 |
LLEN/HLEN/SCARD/ZCARD | 各类型元素数 | O(1) 安全 |
redis-cli MEMORY USAGE order:20260927:000001
# (integer) 52428944 # 约 50MB
redis-cli DEBUG OBJECT order:20260927:000001
# Value at:0x... refcount:1 encoding:raw serializedlength:52428832
MEMORY USAGE返回键值对整体占用(含 key 与内部结构),比serializedlength更接近真实内存。SAMPLES 越大越精确、开销也越大,批量排查建议取 5~10。
2.4 存量 RDB 离线分析
导出 RDB 用离线工具分析,不干扰线上:redis-cli --rdb /data/backup/redis-$(date +%Y%m%d).rdb 在线导出(异步),再用 rdr 解析:rdr -t file.rdb 查看 Top Key,rdr -l -s 1048576 file.rdb 列出超过 1MB 的 Key。
三、大 Key 拆分方案:Hash 分片与压缩
3.1 拆分原则
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 大 Hash(用户属性) | Hash 分片(sharding) | 按 field 前缀拆成 N 个子 Hash |
| 大 List(消息队列) | 换 Stream / 按业务拆 | List 不适合超长队列 |
| 大 ZSet(排行榜) | 按时间/区间拆 | 拆分后按需合并 |
| 大 String(大对象) | 压缩 / 拆字段 | gzip/zstd 或改存 Hash |
| 大 Set | 按维度拆 | 或改用 HyperLogLog/布隆过滤器 |
3.2 Hash 分片:一个大 Hash 拆成 N 个小 Hash
用户属性原为单 Key user:{id},拆成 128 个分片,路由函数幂等(读写同规则):
# 写入:按 user_id 定位分片
redis-cli HSET user:1001:0 name "张三" age 28 city "杭州"
redis-cli HSET user:1001:47 hobby "骑行" level 5
# 读取:同样规则算分片号再 HGETALL
redis-cli HGETALL user:1001:47
// 分片路由:2 的幂取位与,读写在同一个类里保证一致
public class HashShard {
private static final int SHARD_COUNT = 128;
public static String shardKey(long userId, String prefix) {
int slot = (int) (Long.hashCode(userId) & (SHARD_COUNT - 1));
return String.format("%s:%d:%d", prefix, userId, slot);
}
}
3.3 大 String 的拆分与压缩
| 方案 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| 压缩存储 | 内存省 5~20 倍 | CPU 消耗、不可随机读 | 一次性大对象 |
| 拆成 Hash | 可随机读写字段 | 结构改造大 | 频繁部分更新的对象 |
| 冷热分离 | 热数据小 Key、冷数据换存储 | 架构复杂 | 订单、日志等 |
# 应用侧压缩后再写 Redis
redis-cli SET order:20260927:000001 "$(zstd -c /tmp/payload.json | base64)"
3.4 删除大 Key 的正确姿势
# 错误做法:直接 DEL 大 Key 会阻塞主线程数秒!
# redis-cli DEL order:20260927:000001
# 正确做法:UNLINK 异步释放(Redis 4.0+)
redis-cli UNLINK order:20260927:000001
# redis.conf - 各类删除场景全部启用异步(lazy free)
lazyfree-lazy-eviction yes # 内存淘汰异步
lazyfree-lazy-expire yes # 过期删除异步
lazyfree-lazy-server-del yes # 内部删除异步
四、热 Key 的探测:热点分析与 Proxy 统计
4.1 redis-cli –hotkeys 扫描
# 前提:maxmemory-policy 需为 allkeys-lfu / volatile-lfu
redis-cli CONFIG SET maxmemory-policy allkeys-lfu
redis-cli CONFIG REWRITE
# 扫描全库热 Key
redis-cli --hotkeys
# 热门 Key: "goods:10086" freq: 230 type: string
# 热门 Key: "live:room:9527" freq: 198 type: hash
# 定点检查疑似热 Key
redis-cli OBJECT FREQ goods:10086 # (integer) 230
OBJECT FREQ是 LFU 计数器(0~255),--hotkeys需全库扫描,适合低频巡检,不适合秒级实时定位。
4.2 实时热点统计:Proxy 与日志
在 Proxy(Codis/Twemproxy)或客户端封装层统计命令分布最准确:
# 短时抓包分析命令分布(MONITOR 开销大,慎用)
redis-cli MONITOR | awk '{print $3}' | sort | uniq -c | sort -rn | head -10
4.3 业务侧统计方案对比
| 方案 | 实时性 | 准确性 | 侵入性 | 适用 |
|---|---|---|---|---|
--hotkeys | 低 | 中(相对) | 无 | 定期巡检 |
| Proxy 命令统计 | 高 | 高 | 无(Proxy 层) | 有 Proxy 架构 |
| 客户端埋点 | 高 | 高 | 有(改代码) | 无 Proxy |
| 日志分析 | 中 | 中 | 低 | 离线分析 |
没有 Proxy 时,最实用的是客户端封装层做概率采样计数:对读命令的 Key 按 1% 采样,超过阈值上报热点,既能准确定位又开销小。
五、热 Key 的缓解:本地缓存、分片与多副本
5.1 第一道防线:本地缓存
把热 Key 的 value 缓存到应用本地(Caffeine / Go map + TTL),读请求大部分命中本地,少量穿透到 Redis。
// Caffeine 本地缓存
Cache<String, String> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(5)) // TTL 5s,控制与 Redis 偏差
.build();
// 读路径:先本地,再 Redis,最后 DB
String val = localCache.getIfPresent("goods:10086");
if (val == null) {
val = redis.get("goods:10086");
if (val != null) localCache.put("goods:10086", val);
}
| 参数 | 建议值 | 说明 |
|---|---|---|
| 最大条数 | 1 万~10 万 | 与热点数量匹配 |
| TTL | 3~10 秒 | 越短越一致,越长命中率越高 |
| 淘汰策略 | W-TinyLFU | Caffeine 默认 |
本地缓存引入的一致性问题:同一 Key 在不同实例 TTL 不同,可能短时读到旧值。强一致场景需加版本号校验(见缓存一致性专题)。
5.2 第二道防线:热 Key 分片(影子 Key)
不改变存储结构,读路径给热 Key 加随机后缀,把读压力分散到多个影子 Key:
String readHotKey(String key) {
int slot = ThreadLocalRandom.current().nextInt(16);
String shadowKey = key + ":shadow:" + slot;
String v = redis.get(shadowKey);
if (v == null) { // 影子未命中回源
v = redis.get(key);
if (v != null) redis.setex(shadowKey, 3, v); // 短 TTL 写影子
}
return v;
}
分片方案要求写入方同步维护影子 Key(或写源 Key + 影子短 TTL),实现成本较高,只对极少数超级热点使用。影子 Key 必须设置短 TTL,防止长期不一致。
5.3 治理优先级
| 手段 | 成本 | 见效 | 推荐场景 |
|---|---|---|---|
| 本地缓存 | 低 | 立竿见影 | 绝大多数热 Key |
| 热 Key 分片 | 中 | 明显 | 极少数超级热点 |
| 多副本/扩容 | 高 | 中 | 单分片容量瓶颈 |
| 预热 + 永不过期 | 低 | 中 | 稳定热点 |
六、监控与告警体系建设
6.1 关键监控指标
| 指标 | 获取方式 | 告警阈值 |
|---|---|---|
| 大 Key 数量 | --bigkeys/巡检脚本 | 持续上升即告警 |
| 单 Key 内存 | MEMORY USAGE | 单 Key > 100MB |
| 主线程阻塞 | INFO stats 的 blocked_clients | 出现即告警 |
| 热 Key 频率 | OBJECT FREQ | freq > 200 |
| 慢查询 | SLOWLOG | > 10ms 命令增多 |
6.2 redis_exporter + Prometheus 告警
# redis_exporter:指定上报大小的 Key 前缀
redis_exporter:
command: redis_exporter
args:
- --redis.addr=redis://127.0.0.1:6379
- --check-keys=user:*,order:*,goods:*
ports:
- containerPort: 9121
# Prometheus 告警规则
groups:
- name: redis-bigkey.rules
rules:
- alert: RedisBigKeyDetected
expr: redis_key_size_bytes > 104857600
for: 5m
annotations:
summary: "Redis 发现大 Key: {{ $labels.key }}"
6.3 大 Key 巡检脚本
#!/bin/bash
# bigkey-scan.sh - 定时扫描大 Key 并告警
LOG=/var/log/redis/bigkey-$(date +%Y%m%d).log
THRESHOLD=10485760 # 10MB
redis-cli --scan --count 500 | while read key; do
size=$(redis-cli MEMORY USAGE "$key" SAMPLES 5)
if [ -n "$size" ] && [ "$size" -gt "$THRESHOLD" ]; then
echo "$(date +%F_%T) $key $size" >> "$LOG"
fi
done
[ -s "$LOG" ] && curl -X POST -H "Content-Type: application/json" \
-d "{\"text\":\"Redis 大 Key: $(cat $LOG)\"}" \
https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx
七、完整治理流程与实战案例
7.1 治理流程七步法
1. 现状摸底 -> --bigkeys / --memkeys / rdr 离线分析
2. 确定标准 -> 定义阈值(如 >10MB / freq>200)
3. 定位分析 -> MEMORY USAGE / OBJECT FREQ / SLOWLOG
4. 方案设计 -> 拆分 / 压缩 / 本地缓存 / 分片
5. 灰度改造 -> 新 Key 写入,旧 Key 双写过渡
6. 验证效果 -> 内存水位、p99 延迟、命中率对比
7. 常态监控 -> 巡检 + 告警 + 定期回归
7.2 案例一:订单 Hash 大 Key 拆分
问题:单 Key order:20260927 用 Hash 存全天订单,字段超 10 万,HGETALL 与 DEL 频繁阻塞主线程。
改造:按小时拆分为 order:20260927:10、order:20260927:11 等;读路径按时间定位分片,跨小时并行 HGETALL;旧 Key 用 UNLINK 异步删除。
效果:单 Key 体积从 50MB 降到 <5MB,DEL 阻塞从 2s 降到 <10ms。
7.3 案例二:商品热 Key 本地缓存治理
问题:大促期间 goods:10086 每秒被访问 30 万次,打满单分片网卡。
改造:Caffeine 本地缓存 TTL=5s,命中率约 95%,Redis 侧 QPS 从 30 万降到 1.5 万。
| 阶段 | 峰值 QPS | Redis 侧 QPS | p99 延迟 |
|---|---|---|---|
| 改造前 | 30 万 | 30 万 | 12ms |
| 改造后 | 30 万 | 1.5 万 | 2ms |
7.4 案例三:排行榜 ZSet 分段
用户积分排行榜单 ZSet 有 5000 万成员,ZREVRANGE 只取 Top 100。改造为分段存储:主 ZSet 只存 Top 1 万,其余按积分区间拆成多个小 ZSet。读取 Top N 只查主 ZSet,写入按区间路由,内存与命令耗时大幅下降。
八、大 Key 对集群与复制的影响
8.1 集群场景的放大效应
Cluster 集群中大 Key 的槽位固定落在某分片,带来三重放大:
- 分片倾斜:单节点内存、CPU、网络远高于其他节点,集群容量被"最短木板"锁死
- 迁移阻塞:槽迁移(
CLUSTER SETSLOT)时大 Key 所在槽耗时长,可能阻塞 - 复制放大:主从全量/增量同步时,大 Key 传输拖慢 repl backlog,易触发断连重连
# 查看各节点内存分布,快速识别倾斜
redis-cli --cluster check 127.0.0.1:7000
redis-cli INFO memory | grep used_memory_human # 各节点分别执行
8.2 复制中的大 Key 风险
大 Key 写入主库后,AOF 追加与 RDB 快照都要记录同样大的数据,主从同步同样传输。一个 50MB 的 Key 写一次,落盘与网络开销都是 50MB 量级。因此大 Key 治理应放在"数据写入前"而不是"出事后再拆"。
# 检查主从复制状态与偏移量
redis-cli INFO replication
# master_repl_offset:... / slave_repl_offset:...
九、工具与自动化治理
9.1 治理工具矩阵
| 工具 | 用途 | 说明 |
|---|---|---|
redis-cli --bigkeys | 快速摸底大 Key | 内置 |
redis-cli --memkeys | 内存维度扫描 | 内置(Redis 6.2+) |
rdr | RDB 离线解析 | redis-rdb-tools 生态 |
redis_exporter | Prometheus 指标导出 | 配合 Grafana |
| 自研巡检脚本 | 定时精确扫描 | 见 6.3 脚本 |
9.2 自动化治理框架建议
用 cron 编排三类定时任务:bigkey-scan(每 6 小时,执行 6.3 的巡检脚本)、hotkey-scan(每 2 小时,redis-cli --hotkeys 输出到日志)、rdb-analysis(每周一凌晨,rdr -l -s 1048576 解析上周 RDB 并发送周报邮件)。
9.3 治理效果验证清单
- 全库已无单 Key 内存 > 100MB
- 无 O(N) 命令写入
SLOWLOG - 集群各分片
used_memory方差 < 20% - 热 Key 本地缓存命中率 > 90%
- 大 Key 删除均使用
UNLINK - 巡检脚本与告警已接入监控平台
- 每月回顾大 Key/热 Key 报告
结语
大 Key 与热 Key 是 Redis 生产运维中最常见的两类"隐形杀手",核心要点回顾:
- 界定要量化:为"大"和"热"定义明确阈值(内存 > 10MB、元素 > 1 万、freq > 200),否则无法治理
- 发现要常态化:
--bigkeys/--memkeys快速摸底 + SCAN +MEMORY USAGE精确排查 + rdr 离线分析,三层结合 - 拆分要幂等:Hash 分片的路由函数必须读写一致;删除大 Key 一律用
UNLINK并开启 lazyfree - 热 Key 治理要分层:本地缓存解决绝大多数,影子分片对付超级热点,扩容解决容量瓶颈
- 监控要闭环:指标上报 + 告警阈值 + 定期巡检,把治理从"救火"变成"防火"
治理大 Key 与热 Key 的最高境界,是在架构设计阶段就避免它们:预估单 Key 规模、设置 TTL 与分桶策略、把高并发读的 Key 天然设计成可缓存结构。事后治理是止损,事前设计才是真正的工程能力。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。