Redis 大 Key 与热 Key 全过程治理
Redis 线上事故里,大 Key 和热 Key 非常高频。只会背“大 Key 要拆,热 Key 要本地缓存”不够。真正要懂的是:大 Key 为什么会让 Redis 慢,热 Key 为什么 Cluster 加机器也不一定解决,怎么发现、怎么治理、怎么避免治理时引入数据不一致或业务丢数据。
这一页解决这些问题
| 问题 | 要掌握到什么程度 |
|---|---|
| 大 Key 是什么 | 知道不是 key 名字长,而是 value 太大或元素太多 |
| 热 Key 是什么 | 知道是访问频率远高于其他 key,单点流量集中 |
| 为什么危险 | 能讲清网络、单线程、内存、删除、复制、Cluster 热点 |
| 怎么发现 | 会用 --bigkeys、MEMORY USAGE、SLOWLOG、INFO、应用侧统计 |
| 怎么治理 | 会拆 Key、分页、限长、异步删除、本地缓存、请求合并、限流 |
| Cluster 下怎么办 | 知道单 key 只能落一个 slot,加机器不一定解决热 Key |
| 项目怎么落地 | 能设计商品详情、字典配置、资产字段、排行榜的缓存模型 |
大 Key 和热 Key 的区别
| 对比 | 大 Key | 热 Key |
|---|---|---|
| 核心问题 | 单次操作太重 | 单位时间访问太高 |
| 典型例子 | 一个 String 存 5MB JSON,一个 Hash 有几十万 field | 秒杀商品、热门商品详情、全站配置 |
| 主要危害 | 网络慢、命令慢、删除阻塞、复制和迁移慢 | 单节点 CPU/网络高、请求排队、缓存击穿风险 |
| Cluster 是否能解决 | 不能根治,单个大 Key 仍在一个 slot | 不能根治,单个热 Key 仍在一个 slot |
| 常见治理 | 拆 Key、分页、限长、UNLINK | 本地缓存、多级缓存、请求合并、拆热点、限流 |
一个 Key 也可能既大又热,比如热门商品详情里塞了大量评论、规格、推荐列表,还被首页高频访问。这种最危险:单次请求重,访问频率还高。
大 Key 是什么
大 Key 不是 key 名字很长,而是 value 太大。
| 类型 | 大 Key 示例 | 危险命令 |
|---|---|---|
| String | product:detail:1001 存几 MB JSON | GET、SET |
| Hash | tenant:fields:1001 有几十万字段 | HGETALL、HKEYS |
| List | user:messages:1001 有几十万元素 | LRANGE 0 -1 |
| Set | activity:users:1001 有大量用户 ID | SMEMBERS |
| ZSet | rank:global 有百万成员 | 大范围 ZRANGE、ZREVRANGE |
大 Key 没有全行业统一阈值,要结合机器配置、网络、序列化、业务 SLA 判断。常见经验:
- String 超过几十 KB 就要关注,超过几百 KB 要谨慎,MB 级通常要治理。
- Hash、Set、List、ZSet 元素数超过几千到几万要关注。
- 单次返回结果越大,风险越高。
- 删除、迁移、复制时的风险往往比读取时更隐蔽。
大 Key 为什么会拖慢 Redis
Redis 核心命令执行通常是单线程顺序处理。一个大 Key 命令执行时间长,后面的命令就要排队。
flowchart TD
A["客户端请求 HGETALL big:hash"] --> B["Redis 主线程执行命令"]
B --> C["遍历大量 field"]
C --> D["组装大响应"]
D --> E["网络传输大量数据"]
E --> F["后续命令排队等待"]大 Key 会在多个环节放大成本:
| 环节 | 为什么变慢 |
|---|---|
| 命令执行 | 需要遍历大量元素或处理大对象 |
| 网络传输 | 响应体大,输出缓冲区压力大 |
| 客户端反序列化 | Java 对象解析、JSON 解析变慢 |
| 删除释放 | DEL 同步释放大对象可能阻塞主线程 |
| AOF/RDB | 持久化文件变大,rewrite 成本变高 |
| 主从复制 | 大对象写入复制慢,复制缓冲区压力大 |
| Cluster 迁移 | slot 迁移时大 Key 搬迁慢 |
大 Key 怎么发现
redis-cli -h 127.0.0.1 -p 6379 --bigkeys
MEMORY USAGE product:detail:1001
SLOWLOG GET 20
INFO commandstats
INFO memory不要在生产执行:
KEYS *应该使用渐进扫描:
SCAN 0 MATCH product:* COUNT 1000注意:SCAN 也不是完全无成本,可能返回重复 key,需要应用侧容忍或去重。生产扫描要低峰、限速,最好走从库或离线分析。
大 Key 怎么治理
核心原则:不要让一个 key 承担无限增长的数据,也不要一次取回远超页面需要的数据。
| 场景 | 错误设计 | 改进设计 |
|---|---|---|
| 商品详情 | 一个 Key 塞基础信息、评论、推荐、库存 | 基础信息、评论页、推荐列表拆开 |
| 机构字段 | 一个 Hash 存某机构所有表所有字段 | 按机构、表、分页拆 Key |
| 消息列表 | 一个 List 无限追加 | 只缓存最近 N 条,历史走数据库 |
| 排行榜 | 一个 ZSet 存永久全量 | 按日、周、月拆分,只保留 TopN |
| 删除大对象 | DEL big:key | UNLINK big:key |
异步删除:
UNLINK tenant:fields:1001UNLINK 会先把 key 从 keyspace 解除,再由后台线程释放内存,降低主线程阻塞风险。它不是不消耗资源,只是减少同步阻塞。
大 Key 拆分商业 Demo:资产字段缓存
错误设计:
asset:fields:tenant:1001 -> 一个 Hash 存该租户所有库表字段问题:
- 租户数据越多,Hash 越大。
HGETALL一次返回太多。- 更新单表字段时容易影响整个大 Key。
- Cluster 中该 key 只落一个 slot,内存不均。
更好的设计:
asset:field:summary:tenant:1001 -> 统计信息
asset:field:table:tenant:1001:table:2001:page:1 -> 某表第 1 页字段 ID
asset:field:detail:field:3001 -> 单字段详情Java 示例:
public List<FieldDTO> listFields(Long tenantId, Long tableId, int pageNo) {
String pageKey = "asset:field:table:tenant:" + tenantId
+ ":table:" + tableId + ":page:" + pageNo;
List<Long> fieldIds = redis.getList(pageKey, Long.class);
if (fieldIds == null) {
fieldIds = fieldRepository.listFieldIds(tenantId, tableId, pageNo, 50);
redis.set(pageKey, fieldIds, Duration.ofMinutes(20));
}
return fieldIds.stream()
.map(this::getFieldDetail)
.toList();
}
private FieldDTO getFieldDetail(Long fieldId) {
String detailKey = "asset:field:detail:" + fieldId;
FieldDTO cached = redis.get(detailKey, FieldDTO.class);
if (cached != null) {
return cached;
}
FieldDTO dbValue = fieldRepository.findField(fieldId);
redis.set(detailKey, dbValue, Duration.ofMinutes(30));
return dbValue;
}这个设计牺牲了一点读取次数,但换来可控大小、可分页、可局部更新、可局部失效。
热 Key 是什么
热 Key 是访问频率远高于其他 key 的 key。
典型场景:
- 秒杀商品详情。
- 热门文章、热榜、首页配置。
- 全站字典配置。
- 热门资产详情、热门机构配置。
- 某个大客户或大租户的高频数据。
热 Key 的关键不是 value 多大,而是访问太集中。
flowchart TD
A["大量请求访问同一个 key"] --> B["同一个 Redis 节点"]
B --> C["CPU 或网络打满"]
C --> D["请求排队"]
D --> E["应用 Redis 连接池等待"]
E --> F["接口超时"]为什么 Cluster 不一定解决热 Key
Redis Cluster 通过 hash slot 分散 key。一个 key 只会落到一个 slot,一个 slot 由一个 Master 负责。
flowchart TD
A["hot:product:1001"] --> B["计算 hash slot"]
B --> C["slot 8888"]
C --> D["Master-2"]
E["大量请求"] --> D所以加机器能扩整体容量,但不能把同一个热 Key 自动拆到多个节点上。这个问题和 Kafka 单分区热点很像:整体看有很多节点,局部热点仍然集中在一个处理单元。
热 Key 怎么发现
| 方法 | 说明 |
|---|---|
| 应用侧统计 | 在缓存访问封装层统计 key 模板和访问次数 |
| 网关/接口统计 | 热接口通常对应热 Key |
Redis --hotkeys | 需要 LFU 相关配置支持,适合辅助判断 |
| 代理层统计 | 代理或网关统计 TopN |
| 节点监控 | 某个节点 CPU、网络远高于其他节点 |
| 慢日志 | 不一定能发现热 Key,因为单次命令可能不慢 |
生产里最推荐应用侧做 key 模板统计。例如:
product:detail:{id} -> 120000 次/分钟
dict:global:all -> 80000 次/分钟不要把完整高基数 key 都打到日志里,否则日志量会爆。可以按 key 模板聚合,并对 TopN 具体 key 做采样。
热 Key 怎么治理
| 方案 | 适用场景 | 注意点 |
|---|---|---|
| 本地缓存 | 字典、配置、商品详情等读多写少 | 一致性只能做到短暂最终一致 |
| 多级缓存 | Caffeine + Redis + DB | 要有失效广播或短 TTL |
| 请求合并 | 热点过期瞬间大量回源 | 同一 key 只允许一个线程重建 |
| 逻辑过期 | 允许短时间旧值 | 不适合账户余额、库存强一致判断 |
| 拆分 Key | 热点计数、库存分片 | 读取时可能要聚合 |
| 限流降级 | 秒杀、热点活动 | 要有业务降级文案或兜底 |
| 预热 | 活动前已知热点 | 避免冷启动打 DB |
热 Key 治理 Demo:本地缓存 + 短 TTL
适合字典、配置、热门详情这类允许短时间旧值的数据。
public class DictCacheService {
private final Cache<String, DictDTO> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(30))
.build();
public DictDTO getDict(String dictCode) {
String key = "dict:" + dictCode;
DictDTO local = localCache.getIfPresent(key);
if (local != null) {
return local;
}
DictDTO redisValue = redis.get(key, DictDTO.class);
if (redisValue != null) {
localCache.put(key, redisValue);
return redisValue;
}
DictDTO dbValue = dictRepository.findByCode(dictCode);
redis.set(key, dbValue, Duration.ofMinutes(10));
localCache.put(key, dbValue);
return dbValue;
}
}这个方案为什么能抗热 Key:
- 大量请求先命中 JVM 本地缓存。
- Redis 只承受本地缓存失效后的少量请求。
- 本地缓存 TTL 很短,降低长期不一致风险。
不适合支付状态、账户余额、秒杀库存最终判断、权限强一致场景。这类场景要以数据库、状态机或专门库存服务为准。
请求合并防击穿
热 Key 过期时,如果每个线程都回源数据库,会形成击穿。请求合并的目标是:同一个 key 同一时刻只让一个线程重建缓存。
flowchart TD
A["热点 Key 未命中"] --> B["尝试获取重建锁"]
B --> C{"是否拿到锁"}
C -- "是" --> D["查询数据库并写缓存"]
C -- "否" --> E["短暂等待或返回旧值"]
D --> F["释放锁"]
E --> G["再次读缓存"]关键点:
- 锁要有过期时间,避免线程挂了锁不释放。
- 释放锁要校验唯一值,避免误删别人的锁。
- 等待线程不要无限等待,要有超时和降级。
- 高价值接口可以返回旧值,低价值接口可以限流。
治理决策表
线上遇到 Redis 抖动时,不要一看到慢就说“大 Key”,也不要一看到 CPU 高就说“热 Key”。要先分清是“单次操作重”,还是“访问频率高”,还是“缓存未命中导致数据库回源慢”。
flowchart TD
A["Redis 延迟升高"] --> B["看 slowlog"]
B --> C{"是否有单条慢命令"}
C -- "是" --> D["检查命令和 Key 大小"]
D --> E{"value 或集合元素是否很大"}
E -- "是" --> F["按大 Key 治理"]
E -- "否" --> G["检查 Lua、复杂命令、网络返回"]
C -- "否" --> H["看单节点 CPU/网络"]
H --> I{"是否只有某节点特别高"}
I -- "是" --> J["查热 Key 或 slot 热点"]
I -- "否" --> K["看连接池、命中率、DB 回源"]
J --> L["按热 Key 治理"]
K --> M["排查穿透、击穿、雪崩或下游慢"]| 现象 | 更可能是什么 | 第一优先级 | 不要先做什么 |
|---|---|---|---|
SLOWLOG 有 HGETALL、SMEMBERS、大范围 ZRANGE | 大 Key 或全量命令 | 拆 Key、分页、禁止全量读取 | 盲目加 Redis 节点 |
| 单节点 CPU/网卡很高,其他节点正常 | 热 Key 或 slot 热点 | 应用侧 TopN、代理统计、本地缓存 | 只扩容整个集群 |
| Redis CPU 不高,但应用大量 timeout | 连接池等待、网络、DB 回源慢 | 查连接池、命中率、DB QPS | 只看 Redis 服务端 |
| 删除某个 Key 后 Redis 卡顿 | 大 Key 同步释放 | 用 UNLINK,分批删除集合元素 | 继续用 DEL 删大对象 |
| 缓存命中率突然下降,DB QPS 暴涨 | 雪崩、穿透、击穿 | 看 TTL 分布、空值缓存、布隆、热点锁 | 直接把 DB 连接池调大 |
| Cluster 某个 slot 迁移很慢 | 大 Key 或大 slot | 找大 Key,拆分后再迁移 | 高峰期强迁移 |
大 Key 治理优先级
大 Key 的治理目标不是“让 Redis 里没有任何稍大的 Key”,而是让单次命令、网络响应、删除释放、复制迁移都可控。
建议顺序:
- 禁止危险全量命令,例如
HGETALL、SMEMBERS、LRANGE 0 -1、超大范围ZRANGE。 - 把页面查询改成分页或按 ID 列表查询。
- 按业务维度拆 Key,例如按租户、表、日期、页码、分片编号拆。
- 限制集合长度,例如只缓存最近 N 条、TopN、最近 7 天。
- 删除大 Key 使用
UNLINK或分批删除。 - 改数据模型,把详情、列表、统计拆成不同 Key。
- 对历史低频数据回数据库或归档系统,不强行塞 Redis。
示例:排行榜不要无限放一个 ZSet。
错误:rank:global -> 永久全量排行榜
改进:rank:daily:2026-07-05 -> 当日榜
改进:rank:weekly:2026-W27 -> 周榜
改进:rank:global:top1000 -> 只保留 TopN热 Key 治理优先级
热 Key 的治理目标是把访问压力从单个 Redis 节点前移、分散或削峰。
建议顺序:
- 先在应用缓存封装层统计 Key 模板和 TopN 具体 Key。
- 对读多写少数据加本地缓存,例如 Caffeine,TTL 控制在秒级到几十秒。
- 对已知热点提前预热,避免活动开始时冷启动。
- 对热点过期使用请求合并或逻辑过期,避免击穿数据库。
- 对可拆分计数、库存桶、排行榜分段做 Key 分片。
- 对低价值热点接口限流或降级。
- 对强一致数据不要滥用本地缓存,读写仍以事实源为准。
热 Key 拆分要注意一致性
不是所有热 Key 都能随便拆。
| 数据 | 是否适合拆 Key | 原因 |
|---|---|---|
| 商品详情展示 | 适合按基础信息、库存、评价拆 | 各部分更新频率不同 |
| 全站字典 | 适合本地缓存和版本号 | 读多写少 |
| 秒杀库存扣减 | 可以分桶,但最终扣减要严谨 | 要防超卖和重复扣减 |
| 账户余额 | 不适合用 Redis 分片缓存做最终判断 | 强一致要求高 |
| 权限结果 | 可短 TTL 缓存,但关键操作要校验 | 防止权限变更延迟风险 |
如果拆 Key 后需要读取多个分片再聚合,要评估聚合成本和一致性窗口。比如库存分桶可以提升并发,但最终下单仍要有数据库约束、库存服务状态机或补偿机制兜底。
生产排查流程
flowchart TD
A["Redis 超时或延迟升高"] --> B["看是否单节点异常"]
B --> C{"单节点 CPU/网络很高吗"}
C -- "是" --> D["怀疑热 Key 或 slot 热点"]
C -- "否" --> E["看 slowlog 和 commandstats"]
E --> F{"是否有慢命令或大返回"}
F -- "是" --> G["怀疑大 Key 或全量命令"]
F -- "否" --> H["看连接池、网络、持久化、DB 回源"]
D --> I["查应用侧 key 访问 TopN"]
G --> J["查 bigkeys、memory usage、scan"]排查命令:
SLOWLOG GET 20
INFO commandstats
INFO memory
INFO clients
redis-cli --bigkeys
MEMORY USAGE suspected:key应用侧同时看:
- Redis 连接池 active、wait、timeout。
- 接口 P95/P99。
- 缓存命中率。
- 数据库 QPS 和慢 SQL。
- 单个 key 模板访问 TopN。
常见坑
| 坑 | 后果 | 正确做法 |
|---|---|---|
| 一个 Key 存全部数据 | 大 Key 越来越大 | 按业务维度、分页、时间拆分 |
HGETALL 当分页用 | 一次返回全量,阻塞和网络慢 | HSCAN 或业务分页 Key |
删除大 Key 用 DEL | 同步释放阻塞主线程 | 大 Key 用 UNLINK |
| 以为 Cluster 能解决热 Key | 单 key 仍落一个 slot | 本地缓存、拆 Key、限流 |
| 本地缓存不设 TTL | 数据长期不一致 | 短 TTL、广播失效、版本号 |
| 热点缓存永不过期 | 脏数据长期存在 | 逻辑过期、主动失效、补偿刷新 |
| 治理只看 Redis | 可能根因在 DB 回源或连接池 | 全链路看应用、Redis、DB |
面试标准回答
大 Key 指一个 key 对应的 value 太大或元素太多,不是 key 名字长。比如一个 String 存几 MB JSON,一个 Hash 有几十万 field,一个 List/Set/ZSet 有大量元素。大 Key 会导致命令执行慢、网络传输慢、删除阻塞、AOF/RDB 和主从复制压力大,在 Cluster 中还会造成 slot 内存不均和迁移慢。发现可以用 redis-cli --bigkeys、MEMORY USAGE、SLOWLOG、INFO commandstats、SCAN 分批扫描。治理方式是拆 Key、分页取、限制集合长度、避免 HGETALL/SMEMBERS 这类全量命令,删除大 Key 用 UNLINK。
热 Key 指访问频率远高于其他 key 的 key,比如秒杀商品、热门商品详情、全站配置。Redis Cluster 只能把不同 key 分散到不同 slot,同一个热 Key 仍然落在一个 slot 和一个 Master 上,所以单纯加机器不一定解决。热 Key 要通过应用侧访问统计、节点 CPU/网络、代理层统计、--hotkeys 等方式发现,治理方式包括本地缓存、多级缓存、热点预热、请求合并、逻辑过期、拆分 Key、限流降级。
大 Key 是单次操作重,热 Key 是访问频率高。一个 key 可以既大又热,这种最危险。生产排查要结合 slowlog、bigkeys、memory usage、节点 CPU/网络、连接池、命中率和数据库回源一起看。关联知识点
| 知识点 | 说明 |
|---|---|
| Redis 高并发缓存治理 | 综合治理总览 |
| Redis 命令执行与数据结构原理 | 为什么慢命令、大 Key 会阻塞 |
| 过期删除与内存淘汰 | TTL、淘汰、大 Key 删除风险 |
| 缓存问题 | 穿透、击穿、雪崩总览 |
| 主从哨兵 Cluster 全过程 | Cluster 下 slot、MOVED、热点限制 |
