大 Key 与热 Key 治理:发现、拆分与缓解全流程

Redis 大 Key 与热 Key 治理实战:大 Key 危害(阻塞/内存不均/网络)、大 Key 发现(bigkeys/scan/memory usage)、大 Key 拆分方案(Hash 分片/压缩)、热 Key 探测(热点分析/Proxy 统计)、热 Key 缓解(本地缓存/分片/多副本)、监控告警与完整治理流程

在生产 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 keyserializedlength、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 万与热点数量匹配
TTL3~10 秒越短越一致,越长命中率越高
淘汰策略W-TinyLFUCaffeine 默认

本地缓存引入的一致性问题:同一 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 FREQfreq > 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 万。

阶段峰值 QPSRedis 侧 QPSp99 延迟
改造前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+)
rdrRDB 离线解析redis-rdb-tools 生态
redis_exporterPrometheus 指标导出配合 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 生产运维中最常见的两类"隐形杀手",核心要点回顾:

  1. 界定要量化:为"大"和"热"定义明确阈值(内存 > 10MB、元素 > 1 万、freq > 200),否则无法治理
  2. 发现要常态化:--bigkeys/--memkeys 快速摸底 + SCAN + MEMORY USAGE 精确排查 + rdr 离线分析,三层结合
  3. 拆分要幂等:Hash 分片的路由函数必须读写一致;删除大 Key 一律用 UNLINK 并开启 lazyfree
  4. 热 Key 治理要分层:本地缓存解决绝大多数,影子分片对付超级热点,扩容解决容量瓶颈
  5. 监控要闭环:指标上报 + 告警阈值 + 定期巡检,把治理从"救火"变成"防火"

治理大 Key 与热 Key 的最高境界,是在架构设计阶段就避免它们:预估单 Key 规模、设置 TTL 与分桶策略、把高并发读的 Key 天然设计成可缓存结构。事后治理是止损,事前设计才是真正的工程能力。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「redis」更多文章

  1. Redis 向量检索实战:RediSearch、HNSW 与 Embedding 管道集成
  2. 缓存一致性终极方案:双删、binlog 订阅与最终一致性架构
  3. 高级数据结构实战:Bitmap、HyperLogLog、GEO、布隆过滤器与 Stream