热点 Key vs 大 Key:别搞混了
热点 Key 是指被大量客户端频繁访问的 Key,QPS 远高于其他 Key,导致单个 Redis 节点成为瓶颈。
| 维度 | 热点 Key | 大 Key |
|---|---|---|
| 问题本质 | 访问频率过高 | 数据体积过大 |
| 核心危害 | 单节点 CPU/网络被打满 | 主线程阻塞、网络拥塞 |
| 典型场景 | 秒杀商品、热搜话题 | 10MB 的 JSON、百万级集合 |
| 解决思路 | 分散访问压力 | 拆分数据体积 |
一句话结论 :热点 Key 的核心问题是访问集中在一个节点上,解决方案是本地缓存(减少请求)+ 读写分离(分散读压力)+ Key 分片(分散到多个节点) 。
热点 Key 的三种死法
单节点过载 :热点 Key 集中在某个节点,该节点 CPU 和网卡带宽被瞬间打满,其他节点空闲。
邻居效应 :同一节点上的其他 Key 响应变慢,一个热点 Key 拖垮一整片业务。
主从同步延迟 :热点 Key 涉及写操作时,主节点压力大导致复制延迟,从节点读到旧数据。
典型场景:
- 突发热点 (明星官宣、热搜):不可预测,最危险
- 预期内热点 (秒杀、限时抢购):可以提前准备
- 架构缺陷 (全局配置放单个 Key):人为制造的热点,改设计就能解决
怎么发现热点 Key
方法一:redis-cli --hotkeys(Redis 4.0+)
bash
# 需要先开启 LFU 淘汰策略
redis-cli --hotkeys
# 输出示例:
# [Hot Key] user:profile:10086 (access count: 583210)
# [Hot Key] seckill:goods:1001 (access count: 421305)
基于 Redis 的 LFU 计数器统计,前提是淘汰策略必须设为 volatile-lfu 或 allkeys-lfu。
方法二:MONITOR 命令(临时排查,慎用)
bash
redis-cli MONITOR | grep "GET|HGET" | awk '{print $NF}' | sort | uniq -c | sort -nr | head -20
严重警告 :MONITOR 在高 QPS 下会导致 10%~30% 性能下降,只能短时间使用。
方法三:业务层监控(推荐长期方案)
ini
public class HotKeyDetector {
// Guava Cache 做 1 秒滑动窗口
privatefinal Cache<String, AtomicLong> counterCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.build();
privatestaticfinallong HOT_THRESHOLD = 1000;
public void recordAccess(String key) {
AtomicLong counter = counterCache.get(key, () -> new AtomicLong(0));
long count = counter.incrementAndGet();
if (count == HOT_THRESHOLD) {
alertService.warn("检测到热点 Key:" + key + ",QPS:" + count);
}
}
}
对 Redis 零侵入,适合长期运行,可结合 Prometheus 做可视化。
四种解决方案
方案一:本地缓存(最优先)
在应用服务器本地缓存热点数据,请求直接从内存返回,不经过 Redis:
typescript
public class LocalCacheSolution {
privatefinal Cache<String, String> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.SECONDS) // 5 秒过期,容忍短暂不一致
.build();
public String getHotData(String key) {
// 1. 先查本地缓存
String value = localCache.getIfPresent(key);
if (value != null) return value;
// 2. 本地 miss,查 Redis
value = redis.get(key);
if (value != null) {
localCache.put(key, value);
return value;
}
// 3. Redis 也 miss,查 DB
value = db.query(key);
if (value != null) {
redis.set(key, value, 30, TimeUnit.MINUTES);
localCache.put(key, value);
}
return value;
}
}
热点 Key 的 Redis QPS 可能从 30 万降到几千。适用于数据量小、更新不频繁的场景。
方案二:Key 分片
将一个热点 Key 复制为 N 个副本,分布在不同节点:
arduino
public class HotKeyShardingSolution {
privatestaticfinalint SHARD_COUNT = 16;
// 写入时:同时写 N 个分片
public void setHotKey(String key, String value) {
for (int i = 0; i < SHARD_COUNT; i++) {
redis.set(key + ":shard:" + i, value, 30, TimeUnit.MINUTES);
}
}
// 读取时:随机选一个分片
public String getHotKey(String key) {
int shard = ThreadLocalRandom.current().nextInt(SHARD_COUNT);
return redis.get(key + ":shard:" + shard);
}
}
30 万 QPS 分 16 片,每个节点只承受约 1.9 万。代价是写入开销和内存都翻 N 倍,适合读远多于写的场景。
方案三:读写分离 + 多级缓存(京东方案)
三级协同:本地缓存挡 90% → Redis 从节点挡 9% → DB 熔断兜底 1%。30 万 QPS 的热点 Key 也能稳定应对。
方案四:限流降级(兜底)
对热点 Key 的访问频率限流,超阈值直接返回降级数据。实现简单,但部分用户会看到降级内容。
方案对比与生产推荐
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 本地缓存 | 性能最好,直接挡在应用层 | 多实例数据不一致 | 数据量小、更新不频繁 |
| Key 分片 | 读写都能分散 | 写入开销大,维护成本高 | 读远多于写 |
| 读写分离 | 对应用透明 | 主从延迟 | 有从节点资源 |
| 限流降级 | 兜底保命 | 部分用户看到降级数据 | 配合其他方案使用 |
生产推荐组合 :本地缓存(首选)+ 读写分离(分散读压力)+ 限流降级(兜底保命)。
面试高频追问
追问一:本地缓存和 Redis 的一致性怎么保证?
本地缓存过期时间设短(3~5 秒),容忍短暂不一致。要求高的场景,用 Redis Pub/Sub 或 MQ 广播通知所有实例失效本地缓存。大多数热点数据(商品详情、文章内容)短暂不一致是可接受的。
追问二:热点 Key 和大 Key 同时存在怎么办?
先拆大 Key(减小体积),再分散热点(减少单节点压力)。两者解决思路完全不同,但可能同时出现在同一个 Key 上。