外观
Redis 大 key 治理实战:3 步发现、4 种拆分与热 key 本地缓存兜底
集群某个分片内存使用率常年 90% 而其他分片只有 30%,为了清理一个几 GB 的历史 Hash 有人在业务高峰期敲了 DEL,整个实例卡顿了近十秒,所有依赖它的服务超时告警齐飞——这是大 key 治理缺位的两个典型代价。大 key 不等于「业务上重要的 key」,而是指单 key 占用内存过大或元素过多的 key:它们在写入时拉长主线程耗时,在删除和过期时造成阻塞,在集群里造成内存与流量的倾斜。与之相伴的热 key 则是另一种失衡:单个 key 的 QPS 高到打满单节点 CPU 或网卡带宽。这篇文章给出一套「发现、拆分、兜底」的完整打法。
先认清危害与判定线:为什么必须治
大 key 的危害贯穿生命周期。写入时,超大集合的序列化与内存分配拉长命令耗时,Redis 单线程模型下所有请求跟着排队;主从同步时,大 key 的传播与全量同步的 RDB 传输放大网络压力;过期和删除时,同步 DEL 与过期回收会造成秒级阻塞;集群模式下,大 key 所在分片的内存、带宽都被单点吃满。业界常用的参考判定线是:String 类型 value 超过 10KB、集合类(Hash/List/Set/ZSet)元素数超过 5000 或估算内存超过 1MB,就值得登记治理台账。热 key 的判定更直接:单个 key 的 QPS 达到单节点承载能力的三成以上,或所在节点带宽接近网卡上限,就需要介入。两条判定线都不是硬性标准,但「没有任何线」的团队往往直到出事故才第一次测量。
3 步发现:从采样到全量摸清家底
第一步,在线采样。 redis-cli 自带的两个模式适合日常巡检:--bigkeys 遍历 keyspace 并报告每种类型中元素数或长度最大的 key,--memkeys 则直接按内存占用排序(需要 Redis 6.0 以上)。采样有性能开销,建议加 -i 参数控制间隔,在低峰期执行:
bash
# 每类结构报告最大的 key,-i 0.01 控制扫描节奏降低影响
redis-cli -h 10.0.0.8 -p 6379 --bigkeys -i 0.01
# 按内存占用排序,更贴近治理目标
redis-cli -h 10.0.0.8 -p 6379 --memkeys第二步,定点确认。 采样只给线索,治理前要用精确手段确认:先用 TYPE 看类型,再用 MEMORY USAGE 按类型测量真实内存,配合 LLEN、HLEN、SCARD、ZCARD 看元素数。MEMORY USAGE 默认采样部分元素以加快速度,对小 key 精度足够,对确认为大 key 的对象加 SAMPLES 0 可以拿全量精确值。
第三步,离线全量分析。 在线扫描对超大规模集群太慢,更彻底的做法是拉取 RDB 文件离线分析:用 redis-rdb-cli 这类工具直接解析 RDB,输出内存占用 Top N 的 key 清单,全程不影响线上服务。离线清单、慢日志和客户端埋点三者交叉验证——慢日志里的 HGETALL 耗时毛刺、客户端统计里的异常大响应,往往能暴露采样漏掉的问题 key。三步做完,把确认的 key 按「类型、大小、业务归属、最后访问时间」登记成治理台账,后面的拆分才有优先级依据。
4 种拆分方案:按业务形态对症下药
拆分方案没有万能式,按数据形态选:
| 方案 | 适用形态 | 核心思路 | 迁移要点 |
|---|---|---|---|
| Hash 分桶 | 超大 Hash、field 数万级 | 按 field 哈希取模拆到 N 个子 key | 双写过渡,灰度切读 |
| 大 String 分片 | 单 value 几 MB 的文本/大对象 | 压缩后按块切分或转对象存储 | 编解码封装进工具类 |
| 集合范围拆分 | 时间序列型 List/ZSet | 按天/按 ID 段落位成子 key | 查询层聚合多个子 key |
| 冷热分离 | 少数热数据 + 大量冷数据 | 冷数据归档回数据库或对象存储 | 归档任务幂等可重跑 |
以最常见的 Hash 分桶为例,把一个百万 field 的用户属性 Hash 拆成 64 个子 key:
bash
# 1. 分批扫描大 Hash,COUNT 控制每批数量避免阻塞
HSCAN big:user:props 0 COUNT 500
# 2. 按业务键计算桶号,写入对应子 key(bucket = CRC32(userId) % 64)
HSET user:props:{bucket} userId:1001 nickname dev level 6
# 3. 迁移完成后,用异步删除清理旧 key,绝不在高峰期同步 DEL
UNLINK big:user:props三个关键细节:HSCAN 的 COUNT 只是建议值,客户端代码要循环到游标归零才结束;迁移期间必须双写——写路径同时更新新旧两份,读路径灰度切到新 key,确认无误后再 UNLINK 旧 key;桶数要预留增长空间,按当前规模乘以二到三倍选桶数,避免一年后重拆一遍。冷热分离容易被忽略,却是收益最高的一种:很多「大 key」本质是历史数据堆积,把三个月前的签到记录、消息体归档回数据库后,Redis 里只留热数据,问题直接消失。
热 key 兜底:本地缓存与读扩散双管齐下
热 key 的发现同样有现成工具:redis-cli --hotkeys 需要 maxmemory-policy 配置为 LFU 系列策略(如 allkeys-lfu),利用访问频次统计报告热 key;没有 LFU 条件时,在客户端 SDK 层按 key 统计 QPS,或在代理层(如 Codis、自研 proxy)做聚合统计,往往更精确。发现之后的两条缓解路径要按场景组合使用:
路径一,本地缓存兜底。 在应用进程内加一层短 TTL 缓存,把绝大部分重复读挡在 Redis 之前。以 Java 生态的 Caffeine 为例:
java
// 进程内热 key 兜底:容量与 TTL 双上限,3 秒 TTL 控制脏读窗口
private final Cache<String, String> localCache = Caffeine.newBuilder()
.maximumSize(100_000)
.expireAfterWrite(Duration.ofSeconds(3))
.recordStats()
.build();
public String getProduct(String id) {
String key = "product:" + id;
String hit = localCache.getIfPresent(key);
if (hit != null) {
return hit; // 本地命中,不打 Redis
}
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
}
return value;
}本地缓存后,十万 QPS 的热 key 读流量到 Redis 可能只剩几百。代价是 3 秒的脏读窗口和每实例一份内存占用,所以 TTL 与容量上限必须显式配置,绝不能无界缓存。
路径二,读扩散打散。 对无法接受脏读、且单 key 读写都高的场景,把一个 key 复制成 N 个副本写到不同分片:写时全量写 N 份,读时随机选一份,单节点压力被摊到 N 个分片。副本数按单节点承载能力倒推:单节点扛 5 万 QPS,热 key 预计 20 万,就拆 5 到 6 个副本。两条路径可以叠加,本地缓存负责读放大,读扩散负责写与强一致读的分摊。
避坑总结
- 删大 key 一律用 UNLINK。Redis 4.0 起 UNLINK 将释放动作交给后台线程异步执行,同时建议开启 lazyfree-lazy-expire 与 lazyfree-lazy-user-del,让过期和显式删除都走异步路径,从根上消除阻塞。
- --bigkeys 的结果只能当线索。它报告的是「每类一个最大值」,覆盖不了全量;治理必须落到 RDB 离线分析的完整清单上,否则永远在打地鼠。
- 迁移必须双写加灰度。一把梭直接切 key 的团队都经历过「切完发现旧数据没搬全」的回滚之夜;双写过渡期、读切换、观察期、清理旧 key,四个阶段缺一不可。
- 本地缓存必须设上限。没有 maximumSize 和 TTL 的本地缓存,会在热 key 变多或 value 变大时把应用内存撑爆,甚至诱发 Full GC,反而放大故障。
- 热 key 治理要顺着拓扑找根因。副本打散是缓解不是治愈,追查热 key 的业务来源(比如某个运营活动漏了参数校验)经常能让问题直接消失,工程手段留给真正的高频访问场景。
写在最后
大 key 与热 key 治理是典型的「事前一小时,事后十小时」工作:建立判定线、把发现手段做成周期巡检、把拆分方案沉淀成团队 playbook,这三件事在平时做几乎零成本;等到内存倾斜引发主从切换、DEL 卡顿拖垮上下游时,付出的就是故障复盘和彻夜运维。治理台账记得越勤,那晚睡得越香。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。