什么是热点 Key 问题,如何解决?

#中间件#面试八股

什么是热点 Key 问题,如何解决?

结论:热点 Key 指短时间内被极高频访问的单个 key(如秒杀商品、明星微博、爆款库存)。它的危害不是"数据大",而是"流量集中":单分片 CPU 打满、网络带宽被吃光、集群其他节点却空闲。解决思路从便宜到贵依次是:本地缓存 → 读副本 → key 打散 → 限流,且必须先解决"怎么发现热点"。

一、热点 Key vs 大 Key

维度热点 Key大 Key
判定依据QPS 集中度(如单 key 占实例 10% 以上)value 体积或集合元素数
直接后果单节点/单分片 CPU 与带宽打满单次操作变慢、阻塞主线程
集群表现数据倾斜、单槽过热单槽内存倾斜、迁移慢
主要解法本地缓存、副本分摊、key 散列、限流拆分、压缩、分页访问、UNLINK
共同点都会让"整实例抖动",而不是只影响某接口同左

二、怎么发现热点

手段原理代价
redis-cli --hotkeys依赖 LFU 策略,按 OBJECT FREQ 排序必须开启 allkeys-lfu,且是采样
MONITOR打印所有命令极高开销,仅用于短时定位
代理层统计(如 Codis/Twemproxy)在代理处按 key 计数需要额外组件
客户端埋点应用侧按 key 上报 QPS最准确,需改代码
业务预判大促清单、热搜榜单零成本,但可能漏
# 前提:maxmemory-policy 为 allkeys-lfu / volatile-lfu
redis-cli --hotkeys
# 输出示例(按 freq 排序):
#   [00.00%] Hot key 'item:10086' found so far with counter 4291

# 短时抓包式定位(务必限定时间,生产慎用)
timeout 5 redis-cli MONITOR | grep -o '"item:[0-9]*"' | sort | uniq -c | sort -nr | head

三、四级解法

                    ┌──────────────────────────────┐
  秒杀请求 100 万/s → │ L1 本地缓存(Caffeine)        │ 挡住 90%+
                    └──────────────┬───────────────┘
                                   ▼
                    ┌──────────────────────────────┐
                    │ L2 Redis 读副本(主从/多副本) │ 分摊读流量
                    └──────────────┬───────────────┘
                                   ▼
                    ┌──────────────────────────────┐
                    │ key 打散:item:10086:{0..15}   │ 把 1 个热 key 变 16 个
                    └──────────────┬───────────────┘
                                   ▼
                    ┌──────────────────────────────┐
                    │ 限流 + 队列削峰                │ 保护后端与 DB
                    └──────────────────────────────┘
  • 本地缓存:热点数据放 JVM 内,QPS 再高也只打本地内存;代价是多实例不一致,需要短 TTL 或广播失效。
  • 读副本:一主多从,把读请求分散到从库;注意主从延迟,秒杀库存这类强一致读不能走从库。
  • key 打散:把 stock:10086 拆成 stock:10086:0..N,读时随机选一个再求和;写时要么全写,要么用"分桶预扣 + 异步汇总"。适合"读多写少且允许近似"的场景。
  • 限流与削峰:在网关按 key 维度限流,超出部分排队或快速失败,保证系统不被热点拖垮。
// 热 key 打散:把一份计数拆成 N 份,读时聚合
int buckets = 16;
int idx = ThreadLocalRandom.current().nextInt(buckets);
redis.opsForValue().increment("stock:10086:" + idx);   // 写分散

long total = 0;
for (int i = 0; i < buckets; i++) {                    // 读聚合,O(N) 但 N 很小
    String v = redis.opsForValue().get("stock:10086:" + i);
    total += v == null ? 0 : Long.parseLong(v);
}

四、常见追问

  • 问:热点 Key 会不会导致缓存击穿?答:会叠加。热 key 一旦过期,海量请求同时 miss 就是击穿;所以热 key 通常配"逻辑过期 + 异步重建"或干脆不设 TTL。
  • 问:打散后一致性怎么保证?答:读侧接受"分桶求和"的近似或最终一致;若必须精确(如库存),改用"单 key + 本地缓存扣减 + 异步落库"或直接上专用库存服务。
  • 问:Cluster 下热 key 为什么更严重?答:一个 key 只能落一个槽、一个节点,无法像 Kafka 分区那样水平拆;只能靠业务侧打散或读副本。
  • 问:为什么不能只靠加机器?答:热 key 的流量只会打到持有它的那个分片,加节点不会改变路由,瓶颈仍在单节点。
  • 问:热点发现为什么要结合业务?答:监控只能看到"已经热了",业务能提前知道"将会热"(大促、热搜),提前预热与扩容才是治本。
  • 问:本地缓存会不会把热点变成"脏数据放大器"?答:会,如果失效机制不可靠。给 L1 设 1~5 秒 TTL,或走广播失效,把不一致窗口控制在业务可接受范围。

小结

  • 热点 Key 的本质是流量集中,解法核心是"把一份流量拆成多份"或"把请求挡在 Redis 之前"。
  • 发现靠 LFU 采样 + 客户端埋点,治理靠本地缓存 → 副本 → 打散 → 限流。
  • 热 key 与大 key 常常同时出现,治理时要一起看:既拆体积,也拆流量。
笔记加载中…