:热点 Key 探测与多级缓存:突发流量承接方案)
从面状流量到点状流量前四篇治理的都是面状压力总量超预期靠限流分层、熔断止损、降级舍车。但有一类事故长得不一样——总 QPS 没超任何阈值所有压力却集中在同一个键上。场景内容平台热搜榜第一的明星突然塌房平时 0.3% 的查询流量在十分钟内变成 26%而这 26% 全打在同一个缓存键上。Redis 集群按 key 哈希分片单个键永远只落在一个分片上——集群扩十倍那个分片还是被单键打穿接着是它的连接数、它的网卡、它的 CPU。这就是点状流量分片架构天然无法用水平扩容解决的倾斜。本篇讲两件事怎么在热点形成的头几分钟探测到它以及探测到之后靠多级缓存把它接住。热点探测不能靠人眼看监控热点的准确定义通常是单位时间访问量超过集群平均热点水位线若干倍的单个键。探测的工程约束是它必须便宜——你不可能为每个键维护精确计数器1 亿个键的 HashMap 本身就是一个内存炸弹。主流做法是采样计数每 N 个请求抽 1 个进滑动窗口做频次统计用窗口内占比而不是绝对次数做判据绝对阈值在流量涨落时会疯报或漏报占比天然自适应。抽 1/100 的代价是精度损失占比 2% 以下的键会淹没在长尾噪声里——但真正的热点恰恰不怕采样26% 的键被采到的概率依然接近 26%采样只是把噪声地板抬高不改变头部结构。更讲究的实现如各类代理层的 count-min sketch本质也是用概率换内存。importrandomfromcollectionsimportdeque,Counter rndrandom.Random(2026)N,n_keys100000,2000# 幂律需求分布: rank1 是爆点, rank2/3 次热, 其余长尾probs[1.0/(i1)**1.2foriinrange(n_keys)]keys[hot_%04d%iforiinrange(n_keys)]# 真值: 用同一种子跑全量流统计真实 Top5oraclerandom.Random(2026)truthCounter(oracle.choices(keys,weightsprobs,kN))top_truthtruth.most_common(3)print(全流真实 Top3: %s%[(k,c)fork,cintop_truth])print(第一名独占全流 %.1f%% 的请求%(100.0*top_truth[0][1]/N))# 采样探测器: 每 100 个请求取 1 个(共 1000 事件), 60 秒滑动窗口events,t[],0.0foriinrange(0,N,100):krnd.choices(keys,weightsprobs)[0]t0.06*rnd.random()# 1000 个采样事件约 60 秒走完events.append((t,k))WIN,SHARE,MIN_CNT60.0,0.02,10# 窗口内占比 2% 且至少 10 次window,cnt,hotdeque(),Counter(),{}fort_i,kinevents:window.append((t_i,k))cnt[k]1whilewindow[0][0]t_i-WIN:_,oldwindow.popleft()cnt[old]-1ifcnt[k]MIN_CNTandcnt[k]SHARE*len(window):hot[k]max(hot.get(k,0),cnt[k])print(采样事件数 %d, 探测出热点 %d 个%(len(events),len(hot)))fork,vinsorted(hot.items(),keylambdax:-x[1])[:5]:print( 热点 %s 窗口峰值 %d 次采样 - 还原全量约 %d 次/分%(k,v,v*100))运行输出全流真实 Top3: [(hot_0000, 22292), (hot_0001, 9711), (hot_0002, 6057)] 第一名独占全流 22.3% 的请求 采样事件数 1000, 探测出热点 9 个 热点 hot_0000 窗口峰值 236 次采样 - 还原全量约 23600 次/分 热点 hot_0001 窗口峰值 97 次采样 - 还原全量约 9700 次/分 热点 hot_0002 窗口峰值 58 次采样 - 还原全量约 5800 次/分 热点 hot_0003 窗口峰值 43 次采样 - 还原全量约 4300 次/分 热点 hot_0004 窗口峰值 38 次采样 - 还原全量约 3800 次/分1/100 采样、60 秒窗口真实 Top3 全部命中且排名无误——头部热点在幂律分布下大到采样都掩不住。注意还原值 23600 次/分对应约 393 QPS 的真实流量在一个键上这个数字对 MySQL 是灾难对单进程内存是毛毛雨——这正是把热点抬进本地缓存的经济学依据。探测器输出的是名单动作要跟着名单自动走进名单的键下发本地化复制指令掉出名单一段时间后回收。多级缓存各层拦各层的量接住热点的经典结构是三层L1 进程内缓存本地→ L2 共享缓存Redis→ 回源DB/下游。三层各司其职L1 容量极小几千个键、几十 MB只留得住最高频的头部好处是零网络开销、且天然按实例分摊——这正是热点复制的形态同一个键在 10 个应用实例里各存一份单点流量被切成 10 份L2 提供全量共享视图和一致性收敛点L1 的过期比 L2 短得多秒级 vs 分钟级保证本地副本不会长期脱离共享层的更新。用固定种子的流量画像重放三种拓扑看每一层各拦下什么importrandomfromcollectionsimportOrderedDict rndrandom.Random(99)# 热搜事件流量画像: 爆款 sku_0000 占约 26%, 9 个次热各约 1.2%, 其余长尾weights[560][25]*9[1]*1331keys[sku_%04d%iforiinrange(1341)]streamrnd.choices(keys,weightsweights,k20000)classLRU:def__init__(self,cap):self.d,self.capOrderedDict(),capdefget(self,k):ifkinself.d:self.d.move_to_end(k)returnTruereturnFalsedefput(self,k):self.d[k]Trueiflen(self.d)self.cap:self.d.popitem(lastFalse)defreplay(name,local_cap0,n_local10,shared_cap0):locals_[LRU(local_cap)for_inrange(n_local)]iflocal_capelse[]sharedLRU(shared_cap)ifshared_capelseNonel1hl2horigin0foridx,kinenumerate(stream):iflocals_andlocals_[idx%n_local].get(k):l1h1continueifsharedandshared.get(k):l2h1iflocals_:locals_[idx%n_local].put(k)# L2 命中后回写本层 L1continueorigin1ifshared:shared.put(k)iflocals_:locals_[idx%n_local].put(k)nlen(stream)print(%-24s L1 命中 %5.1f%% | L2 命中 %5.1f%% | 回源 %5.1f%%%(name,100.0*l1h/n,100.0*l2h/n,100.0*origin/n))print(请求流 %d 个 (虚拟流量画像, 种子固定)%len(stream))replay(仅共享缓存 cap64,shared_cap64)replay(仅本地缓存 10x16,local_cap16)replay(L1(10x16)L2(64) 多级,local_cap16,shared_cap64)运行输出请求流 20000 个 (虚拟流量画像, 种子固定) 仅共享缓存 cap64 L1 命中 0.0% | L2 命中 36.7% | 回源 63.3% 仅本地缓存 10x16 L1 命中 29.5% | L2 命中 0.0% | 回源 70.5% L1(10x16)L2(64) 多级 L1 命中 29.5% | L2 命中 7.5% | 回源 63.0%三行输出值得逐行读。仅共享缓存36.7% 命中听着还行但爆款那 26% 全挤在同一个 Redis 键所在分片上——命中率是命中率单点是单点两码事。仅本地缓存29.5% 命中基本就是爆款次热且天然摊到 10 个实例但长尾键每台各存各的回源率反而最高70.5%。多级组合是正解L1 吃下 29.5%热点被 10 份本地副本复制Redis 单键压力归零L2 只需再接 7.5%回源 63% 几乎全是长尾——而长尾流量摊在 1341 个键上每个键都微不足道。多级缓存对总量的削减有限63.3%→63.0%对倾斜的削减是决定性的它把一个键的战争化成了一千个键的散步。回源三闸门击穿、失效风暴与逻辑过期剩下的 63% 回源里还藏着三个经典事故。缓存击穿爆款过期瞬间千军万马齐回源。解法是回源加互斥single-flight同一键并发只放一个请求去 DB其余等待或返回旧值。更进一步是逻辑过期键里带一个逻辑到期时间物理 TTL 很长发现逻辑过期时由抢到互斥的实例异步重建其他请求先用旧值——热点键宁可旧一秒不可空窗一万并发。失效风暴同批写入的键 TTL 相同到期时刻齐刷刷失灵。解法简单粗暴——TTL 加 ±10~20% 随机抖动。预热与兜底可预见的热点运营排期里的明星塌房位、晚会节目单提前推入 L1/L2L2 整个抖动时L1 里带秒级过期、允许陈旧的热点副本就是最后防线——实验第三行里 L1 独立扛住的那 29.5%在共享层故障时就是它的全部使命。常见陷阱与清单用绝对 QPS 阈值判热点流量整体涨 5 倍时满屏误报整体跌时漏报——判据用窗口内占比如 1% 即热点。本地缓存不设防弹衣L1 无容量上限大对象堆积 OOM、无淘汰策略LRU/LFU 至少要有、过期长于 L2各实例数据长期分裂。热点复制不做名单回收键掉出热点名单后不清理本地副本10 台机器为过气键白占内存。只测命中率不测单键 QPS监控面板按命令数统计 Redis 一切正常热点事故从来都是平均数陷阱必须看 top key 排行。落地顺序建议先上 top-key 实时监控redis-cli--hotkeys/代理层统计→ 采样探测 名单自动下发 → L1 热点复制 → 回源 single-flight 与逻辑过期 → TTL 抖动与预热例行化。热点解决的是读侧的极端倾斜。写侧也有一个看似简单实则处处是坑的问题高并发下如何生成全局唯一、趋势递增、还不能被猜的 ID——自增 ID 一压测就发现分库分表撞号UUID 一上量就发现索引爆炸。下一篇《高并发流量治理实战6分布式 ID 生成Snowflake 时钟回拨与号段模式》把发号器这件事讲透。参考来源Wikipedia: Count–min sketch: https://en.wikipedia.org/wiki/Count%E2%80%93min_sketchWikipedia: Cache replacement policies: https://en.wikipedia.org/wiki/Cache_replacement_policiesRedis 官方文档: Eviction内存淘汰策略: https://redis.io/docs/latest/develop/reference/eviction/AWS 最佳实践: Caching best practices多级缓存与热点处理: https://aws.amazon.com/caching/best-practices/tags: 缓存, 热点Key, Redis, 高并发本系列已结集为免费专栏《高并发流量治理实战从限流到全链路压测》 https://blog.csdn.net/weixin_67153745/category_13213830.html 系统性进阶推荐付费专栏《提示词工程实战从入门到生产级 Prompt 设计》限时 ¥19.9首篇免费试读https://blog.csdn.net/weixin_67153745/category_13213600.html