在高并发系统中,Redis 作为核心缓存组件,常常面临一个棘手问题:热点 Key。某个 Key 的 QPS 远超其他 Key,导致单个 Redis 节点 CPU 被打满,甚至引发缓存击穿、雪崩。本文将系统讲解热点 Key 的探测手段,以及如何通过本地缓存构建多级方案来根治这一问题。
一、什么是热点 Key,为什么危险
热点 Key 指的是在短时间内被高频访问的 Key。例如秒杀商品详情、明星微博、突发热点新闻等。它的危险在于:
- 单节点瓶颈:Redis 集群虽然做了分片,但单个 Key 只能落在一个节点上,无法通过加节点分摊压力。
- CPU 打满:单 Key 的 QPS 达到数万甚至数十万时,该节点 CPU 飙升,影响同节点其他 Key。
- 缓存击穿:热点 Key 过期瞬间,大量请求直接打到数据库,可能压垮 DB。
因此,发现热点 Key 是第一步,解决热点 Key 才是目的。
二、热点 Key 的探测手段
1. 客户端埋点统计
在 Redis 客户端(如 Jedis、Lettuce)层面做拦截,每次调用前对 Key 做本地计数,定期上报聚合。
优点:实现简单,能精确到业务维度。
缺点:对客户端有侵入,多实例聚合有延迟,且无法感知非本应用发起的访问。
2. Redis 自带命令
MONITOR:实时打印所有命令,但性能损耗大,生产环境慎用,仅适合短时排查。INFO commandstats:统计各命令调用次数,但无法精确到 Key 级别。HOTKEYS(Redis 4.0+,需开启 LFU 淘汰策略):这是最推荐的原生方案。
开启方式:
redis-cli config set maxmemory-policy allkeys-lfu
redis-cli redis-cli HOTKEYS start
HOTKEYS 基于 LFU 计数器,采样统计访问频率,对性能影响极小。执行 HOTKEYS get 即可返回热点 Key 列表。缺点是只能看到频率,无法看到具体 QPS 数值,且需要 LFU 策略支持。
3. 代理层统计
如果使用 Twemproxy、Codis 或自研 Proxy,可以在代理层对 Key 做滑动窗口计数,天然支持多实例聚合。这是大型互联网公司常用的方案,例如有赞的 TMC(Top N Key 统计)。
4. 基于 Redis 的滑动窗口
利用 Redis 自身的 ZSET 或 INCR + 过期时间,在业务代码中记录 Key 的访问次数:
-- 每次访问时执行
local key = KEYS[1]
local cnt = redis.call('INCR', key)
if cnt == 1 then
redis.call('EXPIRE', key, 60)
end
return cnt
统计每分钟访问量超过阈值的 Key,即为热点。缺点是本身也增加了 Redis 压力,适合作为辅助手段。
5. 京东 HotKey 框架
开源框架 jd-hotkey 采用「客户端上报 + 服务端聚合 + 规则推送」的架构,能实时探测热点 Key 并自动推送到客户端本地缓存,是生产级成熟方案,值得深入研究。
三、本地缓存多级方案
探测到热点 Key 后,核心解决思路是:把热点数据放到离用户更近的地方——本地缓存(JVM 进程内缓存),让请求不再穿透到 Redis。
1. 多级缓存架构
典型的 L1 + L2 架构:
- L1:本地缓存(Caffeine、Guava Cache、Ehcache),进程内,纳秒级访问。
- L2:Redis 集群,分布式缓存,毫秒级访问。
- L3:数据库,兜底存储。
请求流程:
请求 -> L1 本地缓存 -> 命中则返回
-> 未命中 -> L2 Redis -> 命中则回填 L1 并返回
-> 未命中 -> L3 DB -> 回填 L2 和 L1
2. Caffeine 本地缓存示例
Cache<String, String> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.SECONDS)
.recordStats()
.build();
public String get(String key) {
// 1. 查本地缓存
String value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// 2. 查 Redis
value = redis.get(key);
if (value != null) {
localCache.put(key, value);
return value;
}
// 3. 查 DB 并回填
value = db.query(key);
if (value != null) {
redis.setex(key, 300, value);
localCache.put(key, value);
}
return value;
}
3. 本地缓存带来的新问题
引入本地缓存并非银弹,需要重点解决以下问题:
(1)数据一致性
本地缓存分散在多个 JVM 中,Redis 更新后如何通知各节点失效?
- 方案一:Redis Pub/Sub。数据变更时发布消息,各节点订阅后删除本地缓存。
- 方案二:MQ 广播。通过 RocketMQ / Kafka 广播消息,可靠性更高。
- 方案三:短 TTL。本地缓存设置 3~5 秒过期,用时间换一致性,适合对一致性要求不高的场景。
(2)内存占用
本地缓存不能无限增长,必须设置 maximumSize 和淘汰策略(W-TinyLFU),否则可能 OOM。
(3)冷启动
服务刚启动时本地缓存为空,大量请求会打到 Redis。可通过预热机制,在启动时加载已知热点 Key。
(4)热点漂移
热点 Key 是动态变化的,需要探测系统持续工作,动态调整本地缓存的内容。
4. 动态热点本地缓存
结合前面提到的 jd-hotkey 或自研探测系统,可以实现:
- 探测系统实时统计热点 Key。
- 将热点 Key 规则推送到各应用实例。
- 应用实例只对这些 Key 开启本地缓存,并主动从 Redis 拉取最新值。
- 非热点 Key 仍走 Redis,避免本地缓存膨胀。
这样既解决了热点问题,又控制了内存和一致性成本。
四、面试高频追问
Q1:本地缓存和 Redis 数据不一致怎么办?
答:根据业务容忍度选择。强一致场景用 Pub/Sub 或 MQ 广播失效;弱一致场景用短 TTL(1~5 秒)。注意,本地缓存适合「读多写少、能容忍短暂不一致」的数据。
Q2:热点 Key 突然失效怎么办?
答:使用逻辑过期 + 互斥锁。逻辑过期指 value 中存过期时间,物理上不过期,发现逻辑过期后异步刷新,同时返回旧值,避免大量请求同时回源。
Q3:如何防止热点 Key 打满单个 Redis 节点?
答:除了本地缓存,还可以做 Key 拆分,例如 hotkey:1、hotkey:2 分散到不同节点,读取时随机选一个。但更推荐本地缓存方案,因为拆分后更新和一致性更复杂。
Q4:Caffeine 和 Guava Cache 怎么选?
答:Caffeine 在命中率和性能上全面优于 Guava Cache,采用 W-TinyLFU 淘汰算法,是当前 Java 本地缓存的首选。Guava Cache 已基本被 Caffeine 取代。
Q5:多级缓存如何监控命中率?
答:Caffeine 提供 recordStats(),可获取 hitRate、evictionCount 等指标;Redis 通过 INFO stats 查看 keyspace_hits / keyspace_misses。两者结合,接入 Prometheus + Grafana 做可视化。
五、总结
热点 Key 问题的本质是流量集中与存储分片之间的矛盾。解决路径分两步:
- 探测:用
HOTKEYS、代理层统计或 jd-hotkey 框架实时发现热点。 - 治理:用 Caffeine 本地缓存构建 L1 + L2 多级架构,配合 Pub/Sub 或短 TTL 解决一致性,用动态热点推送控制内存。
面试中回答这类问题,建议遵循「先探测、后治理、再讲一致性和兜底」的结构,同时能说出具体框架和代码细节,就能体现出真实的工程经验。
未经允许不得转载:任鹏个人博客 » Redis 热点 Key 探测与本地缓存多级方案实战

