2995 字
约 9 分钟
15
S06-A13 · 缓存的两种用法:加速与削峰不是一回事

"加了缓存,P99 从几百毫秒掉到几十毫秒,可大促还是被打挂了。"这不是缓存没起作用,而是它起的是另一种作用。加速缓存的目标是命中率,削峰缓存的目标是泄压能力;前者的容量按工作集算,后者的容量按并发洪峰算。两者共用同一个实例、同一份 TTL、同一条降级路径时,机制只会对其中一个有效。

要盯住的不是"缓存命中率多少",而是:这套缓存真正服务的是哪个目标,被牺牲的那个目标,凭什么一直没被发现。


1. 两个目标,两套容量算法

加速回答"平均请求快不快",削峰回答"洪峰那几十毫秒里,下游还活着吗"。两者的输入变量完全不同:

比较项 加速用法 削峰用法
目标量 命中率、P99 延迟 峰值吸收率、回源请求峰值
容量按什么算 工作集(活跃键集合) 并发洪峰 × 单次回源耗时
数据特征 热点集中、更新少 瞬时重复率高、可合并
过期策略 可以长,旧一点代价小 必须短,否则旧值放大不一致
失效后果 变慢 下游被打穿
成功判据 请求不出进程 洪峰期间回源数被压到下游容量以下
// 加速:容量 = 工作集 × 放大系数(典型量级 1.2–2,按对象大小实测核对)
容量 ≈ Σ(热键数 × 单键字节) × 1.5

// 削峰:容量与被缓存的数据量无关,与"同时进来的请求数"有关
容量 ≈ QPS峰值 × 单次回源耗时 × 合并率
       // 合并率为 0 时,缓存容量再大也不削峰,只是把请求排队到缓存后面

第二行是全部误区的来源:削峰缓存的容量甚至可以远小于工作集,因为它真正买的不是"数据放得下",而是"同一份数据在峰值窗口内只回源一次"。

两个目的共用一套机制时,结果不是"两个都做到一点",而是两个都达不到:容量按工作集买(加速的算法),过期时间按命中率调(加速的判据),淘汰按最近最少使用(加速的偏好);于是削峰需要的"合并窗口"与"峰值可用的空闲容量"被逐项占掉。被牺牲的是削峰,它不会在月度报表里出现——平时的命中率依旧漂亮,峰值出事时的归因往往是"流量超预期",而不是"缓存被当成了加速器"。

2. 命中率高,不等于能削峰

命中率是窗口内的比例,削峰关心的是窗口内的绝对并发。这两个量可以同时成立又同时失效:

// 命中率:看起来很漂亮,但它对这个窗口内同时有多少请求无感
rate(cache_hits[1m]) / rate(cache_requests[1m])

// 峰值吸收率:真正该看的量
1 - rate(origin_requests[10s]) / rate(cache_requests[10s])
// ─── 用 10s 窗口而不是 1m:削峰的有效性只在那几十毫秒里成立
现象 加速视角的解读 削峰视角的解读
命中率 98% 很好 无意义,2% 落到 20 倍流量上就是 40% 回源
命中率高但回源尖峰仍在 不矛盾 命中的是"分散的键",没合并"同一个键的并发"
缓存里有数据、请求仍打穿 不矛盾 未命中的那批请求同时在建,互不知情

关键区分:**加速靠热点集中生效,削峰靠请求可合并生效。**热点集中让命中率高;请求可合并让并发的重复回源收敛成一次。前者能在平均值上体现,后者只在峰值上体现。所以只看命中率,永远看不到削峰有没有做。

把同一段时间按两种粒度各看一遍,差别就会显形:

// 1 分钟粒度:削峰缺失被平均掉
00:00  命中率 98%   回源 120 QPS   下游容量 500   ── 看起来很安全
// 10 秒粒度:同一分钟里的真实形状
00:00:10  命中率 71%   回源 1400 QPS  下游容量 500  ── 打穿
00:00:20  命中率 99%   回源  40 QPS   下游容量 500  ── 空闲
// ─── 峰值吸收率必须在 10s 甚至 1s 窗口上算,1m 窗口是加速视角的度量

3. 共用一个实例的三个后果

后果 机制 谁被牺牲
峰值时最不耐压的恰恰是加速用的那部分 长 TTL 的热键占了内存与连接,峰值时淘汰把它们挤掉,重建请求集中砸向下游 削峰能力
淘汰策略按加速目标调(LRU/LFU) 峰值期的瞬时重复键是"新键",最容易被淘汰,合并窗口被破坏 削峰能力
降级路径写成了"缓存挂了走 DB" 缓存故障时全部流量同时落库,熔断没设,等于把削峰装置变成单点 可用性,且两套目标同时失效

第三行值得单独说:**"缓存挂了会怎样"在两套目标下的正确答案不同。**加速用法下,答案是"变慢,可接受";削峰用法下,答案是"下游当场被打穿,必须先限流再降级"。同一个回答不可能同时成立,而通常写在文档里的那一句,是按加速场景写的。

// 降级路径必须独立于缓存本身,且默认关闭而不是默认放开
─── fallback:
───   on_cache_error: reject_with_retry_after   // 不是直连下游
───   local_guard: 每实例最多 200 QPS 回源        // 本地阀门,不走共享存储
───   breaker: 下游错误率 > 30% 时打开,半开探测

淘汰策略是最典型的"按加速目标配置、在削峰场景下帮倒忙"的一项:

// 共用实例时,配置项都按加速视角写下
maxmemory 8gb
maxmemory-policy allkeys-lru      // 峰值期涌入的瞬时重复键是"新键",最先被淘汰
// ─── 削峰需要的恰恰是"新键留得住、合并窗口内不消失"
// ─── 若必须共用:给削峰数据单独的 db 或实例,并放宽配额的淘汰权重

4. 过期策略、单飞与降级:只对一边有效的机制

机制 对加速的作用 对削峰的作用 错配后果
长 TTL 提高命中率 放大不一致窗口 削峰拿到了更旧的批量数据
短 TTL 命中率下降 缩短陈旧窗口、提高合并机会 加速侧的收益被自己削掉
单飞(single-flight) 几乎无收益 核心机制 无它则削峰只是"缓存了数据没缓存请求"
请求合并(批处理回源) 无收益 核心机制 缓存命中率高仍会打穿
逻辑过期 + 异步重建 可用 削弱(旧值期间并发仍在) 削峰窗口内旧值被放大
// 单飞:只对"同一个键的并发回源"有效,对"分散键的高命中"毫无帮助
CompletableFuture<T> load(String key) {
    return inflight.computeIfAbsent(key, k ->
        CompletableFuture.supplyAsync(() -> origin.load(k))
            .whenComplete((v, e) -> inflight.remove(k)));   // 完成即摘除
}
// ─── 注意:inflight 本身是本地结构,多实例部署时每实例各合并一次
// ─── 真正的削峰倍数 = 实例数,不是无限

这也是"请求合并只对削峰有效"的准确含义:它把并发变成一次调用,命中率指标上不会出现任何变化,所以在以命中率为唯一 KPI 的团队里,它永远排不进需求。

穿透、击穿、雪崩这三个老问题,在两套目标下的处置优先级也不同:

问题 加速视角的处置 削峰视角的处置 差异根源
穿透(查不存在的键) 空值缓存、布隆过滤器 同上,但必须限制回源并发 加速怕慢,削峰怕同时
击穿(单键重建) 互斥重建,代价是延迟 单飞是核心,且要限排队长度 加速可以等,削峰不能排队
雪崩(大量同时过期) 加随机抖动 过期时间本来就要短,抖动幅度必须更大 削峰侧 TTL 更短,集中过期更频繁

同一套手段,在两个目标下的参数取值相差一个量级:**抖动幅度、互斥粒度、排队上限,全都要按目标重算。**照搬加速场景的参数,等于把削峰窗口留给了下游。

5. 排错:五种现场

现象 真实原因 处理方向
大促回源 QPS 是平时的十几倍 命中率高但未合并,峰值全是同键并发 上单飞,再按实例数校正预期
缓存故障后下游直接崩 降级路径直连下游、无限流 降级默认拒绝 + 本地阀门
加了缓存 P99 反而抖动 大键/热键淘汰引发集中重建 拆键、预热,或把加速与削峰数据分实例
数据陈旧投诉集中在大促 为命中率调长了 TTL,削峰场景下被放大 两套 TTL:加速长、削峰短
扩容缓存后削峰无改善 买的是容量,不是合并能力 削峰的瓶颈在并发不在内存

6. 边界:什么时候不必区分

场景 结论 理由
读多写少的静态数据 只做加速 峰值与均值差异小,合并收益趋零
峰值与均值差异小于 2 倍 只做加速 削峰的容量算法退化,两套机制重合
下游本身能扛住峰值 只做加速 削峰买的是"下游不被打穿",前提不存在
数据可容忍较长陈旧 明确选加速 此时短 TTL 是纯损失
缓存只用于单实例本地 只做加速 单飞退化成本地锁,跨实例合并无从谈起

四个量足以判断这套缓存到底在服务谁:命中率(加速)、峰值吸收率(削峰)、回源请求峰值(削峰的下游视角)、缓存故障时的错误率(降级是否独立)。前两个一起看,才能看出是不是"命中率很高、削峰为零"。


自测

  1. 命中率 98% 而大促回源 QPS 翻十倍,这两个事实为什么不矛盾?
  2. 削峰缓存的容量该按什么算,为什么它可以远小于工作集?
  3. 单飞机制在多实例部署下的真实削峰倍数是多少?
  4. 为提升命中率调长 TTL,对削峰目标造成了什么具体损失?
  5. "缓存挂了会怎样"这个问题,两套目标下的答案差在哪一步?
  6. 什么条件下区分加速与削峰,收益低于管理成本?
S06-A13 · 缓存的两种用法:加速与削峰不是一回事
https://www.clxhxhhr.top/posts/6357/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。