热点数据与突发流量
系统平均负载只有 20%,某几个 key 却把单个缓存分片的 CPU 打满;或者平时平稳,一场秒杀让 QPS 涨了五十倍。热点和突发流量是两种不同的压力形态,但都会在“平均值看起来很正常”的时候把系统搞挂。
什么是热点
热点指访问量高度集中在少数 key、少数分片或少数接口上。典型来源:
- 可预知的:秒杀商品、首页爆款、热搜词、大 V 发布内容。
- 不可预知的:突发新闻、明星事件、被爬虫集中抓取。
- 写热点:点赞、投票、计数器,同一个 key 被高频自增。
热点的破坏力在于“集中”:集群有 100 个分片,流量却全压在其中 1 个上,等于集群缩容了 99%。
热点怎么发现
| 手段 | 做法 | 局限 |
|---|---|---|
| 分片维度监控 | 各分片 QPS/CPU/带宽对比,异常高说明有热点 | 只能定位到分片,需再细化 |
| 客户端埋点统计 | 在缓存客户端按 key 计数,定时上报 TOP N | 有性能开销,可采样 |
| Redis 热点分析 | redis-cli --hotkeys 找出访问频率最高的 key(需配置 LFU 类淘汰策略) | 有一定开销,低频执行 |
| 慢查询日志 | 单 key 数据大、命令重时会进慢日志 | 只能看到慢,不一定看到热 |
| 代理层统计 | 在自研代理/中间件里统计 TOP key | 依赖架构改造 |
| 业务预判 | 运营动作、活动排期、热搜接入 | 只能覆盖可预知热点 |
# 查看热点 key(需要淘汰策略为 LFU 系列,参数以官方文档为准)
redis-cli --hotkeys
# 输出:按访问频率排序的 key 列表及其频率统计
实践建议是“监控 + 预判”双管齐下:可预知的热点提前做准备,不可预知的靠分片监控和客户端埋点发现。
热点读的缓解手段
| 手段 | 原理 | 适用 |
|---|---|---|
| 多副本 key 打散 | 同一份数据写成 N 个 key(hot:1~hot:N),读时随机选一个 | 读多写少的强热点 |
| 本地缓存挡流 | 进程内缓存该 key,直接挡掉绝大多数请求 | 极小、极热、容忍秒级过期 |
| 多级缓存 | CDN → 本地 → Redis → DB 逐层拦截 | 静态或半静态内容 |
| 读写分离 | 读请求分散到多个从库 | 数据来自数据库时 |
| 限流保护 | 网关按 key/用户维度限流 | 兜底,防止打垮依赖 |
多副本 + 本地缓存是应对强热点的组合拳:
// 以下片段需放进使用 Redis 与 Caffeine 的工程中运行
int REPLICA = 8; // 热点数据写 8 份副本
public String getHotGoods(long goodsId) {
String local = localCache.getIfPresent("hot:goods:" + goodsId);
if (local != null) {
return local; // 输出:本地命中,完全不打 Redis
}
int idx = ThreadLocalRandom.current().nextInt(REPLICA) + 1;
String key = "hot:goods:" + goodsId + ":" + idx; // 随机挑一个副本读
String v = redis.get(key);
if (v != null) {
localCache.put("hot:goods:" + goodsId, v); // 回填本地,TTL 设 1~3 秒
}
return v;
}
// 写入时要写全部副本,且允许有短暂不一致,靠短 TTL 收敛
public void setHotGoods(long goodsId, String v) {
for (int i = 1; i <= REPLICA; i++) {
redis.setex("hot:goods:" + goodsId + ":" + i, 300, v);
}
}
多副本的代价是写放大(写 N 次)和一致性变弱,所以只对“读极多、写极少”的数据使用。
热点写怎么办
写热点没法打散成多副本那么简单,因为写要合并回一个真实值。常见做法是分桶累加 + 异步汇总:
点赞数 +1 的流程:
1. 把计数拆成 N 个桶:like:1001:1 ~ like:1001:8
2. 每次点赞随机选一个桶 INCR,压力被摊到 8 个 key 上
3. 后台任务每 1~5 秒把 N 个桶求和,写回 like:1001 的总数
4. 展示时读总数 key(若读也热,就再叠一层本地缓存)
其他手段:
- 异步化:写请求只落入队列,由消费者批量合并后落库(如“同一商品 1 秒内的 1000 次点赞合并成一次 SQL”)。
- 合并写:应用内先攒一小段时间的增量,一次性写库。
- 业务上削峰:把实时计数改为可接受延迟的近似值,前端展示“10 万+”而不是精确值。
突发流量与预案
突发流量(秒杀、推送、开抢提醒)要求提前准备,而不是临时救火:
| 阶段 | 动作 |
|---|---|
| 容量评估 | 按预估峰值算 QPS、连接数、缓存/DB 承载,留 2~3 倍余量 |
| 预热 | 活动开始前把商品、库存、配置加载进缓存,避免开抢瞬间集体回源 |
| 分层限流 | CDN/网关/应用/DB 各层都设阈值,前端排队或发令牌控制进入量 |
| 队列削峰 | 写请求先进 MQ,按下游处理能力匀速消费 |
| 静态化 | 活动页/详情页做成静态资源走 CDN,不经过应用 |
| 降级开关 | 提前准备好关闭非核心功能、返回兜底数据的开关 |
| 压测演练 | 按峰值压测并演练降级,验证阈值与预案可执行 |
| 监控告警 | 大促期间加强监控频率,值班表与升级机制到位 |
热点与雪崩的区别
| 维度 | 热点 | 缓存雪崩 / 击穿 |
|---|---|---|
| 成因 | 流量集中在少数 key 或分片 | 大批 key 同时失效、缓存集群宕机,或单个极热 key 失效瞬间回源 |
| 表现 | 单分片 CPU/带宽打满、响应变慢 | 命中率骤降、数据库 QPS 暴涨 |
| 影响范围 | 局部:与热点相关的请求受影响 | 全局:几乎全部请求回源 |
| 主要对策 | 打散(多副本)、本地缓存、限流 | TTL 加随机抖动、多级缓存、熔断降级、集群高可用、失效时互斥重建 |
| 关键区别 | 是容量问题:压力集中 | 是失效问题:缓存没挡住 |
两者也可能叠加:一个热点 key 恰好过期,同时上万个请求回源,就会从热点演变成击穿。
小结:热点的核心是“压力集中”,解法是打散——多副本 key、本地缓存、多级缓存、读写分离,再用限流兜底;写热点靠分桶累加与异步批量合并。突发流量则靠容量评估、预热、分层限流、队列削峰和降级预案提前准备。记住热点是容量问题、雪崩是失效问题,两者的排查方向和处置手段完全不同。