Skip to content

[L4] 高并发下缓存击穿防护方案如何量化选型?

一句话结论

PHP-FPM 下 Singleflight 跨进程不可行;单 Key 并发 ≤500 用互斥锁兜底;并发 >500 且可接受短暂旧值用逻辑过期。

业务场景

电商大促期间热点商品详情页:

  • 热点 Key 峰值并发:单 Key 3000 并发请求/s(如某明星同款商品上架瞬间)
  • DB 承载上限:500 QPS(正常水位,击穿后瞬间请求全部透传则超限)
  • PHP-FPM 部署:100 Worker × 3 台 = 300 Worker 实例
  • SLA:99.9%,P99 响应 ≤100ms
  • 一致性约束:允许返回旧数据不超过 500ms(用户基本无感知)

体系讲解

缓存击穿的成因

热点 Key 在高并发下恰好过期,导致大量请求同时穿透到 DB。与缓存雪崩(大批 Key 同时过期)不同,击穿通常集中在单个热点 Key,瞬时 DB 压力可达平时的数百倍。

三种防护方案对比

方案核心机制PHP-FPM 可行性并发控制效果响应时延影响返回旧值
Singleflight进程内合并并发请求,仅 1 个协程查 DB⚠️ 仅限单进程(Swoole/Roadrunner),FPM 跨进程不可行极好(进程内 N→1)无额外延迟
互斥锁(Redis SETNX)分布式锁,仅 1 个 Worker 查 DB,其余等待或降级✅ 完全兼容好(全局 N→1)等待期增加延迟可选(降级旧值)
逻辑过期不设 Redis TTL,value 中存 expire_at,过期后异步重建,同时返回旧值✅ 完全兼容好(重建请求 N→1)无阻塞,延迟不受影响是(短暂旧值)

Singleflight 在 PHP-FPM 下为何不可行

Singleflight 的核心是在同一进程内用 Map 记录"正在飞行中"的请求,后续重复请求复用第一个的结果。PHP-FPM 下每个 Worker 是独立进程,进程间不共享内存,300 个 Worker 各自独立发起 DB 查询,无法合并。在 Swoole 协程或 Roadrunner 下,同一 Worker 进程内的协程才能共享 Singleflight Map,有效范围仅限单进程。

逻辑过期方案决策流程

本场景选型

单 Key 并发 3000/s,允许旧值 500ms:选逻辑过期

  • 互斥锁在 3000 并发下,299 个未抢到锁的 Worker 需轮询等待,DB 重建期间 P99 劣化 100–200ms,触碰 100ms SLA 红线
  • 逻辑过期下所有 Worker 立即返回旧值(<500ms 约束内),仅 1 个 Worker 异步触发重建,DB 压力为 1 QPS
  • 代价:短暂(重建期间约 100–300ms)返回旧价格,业务已接受此约束

若业务不允许返回旧值(如库存强一致),则选互斥锁 + 降级返回"稍后再试"而非旧值。

并发量分级选型

单 Key 并发可接受旧值推荐方案
< 100任意互斥锁(轮询等待,影响小)
100–500互斥锁 + 降级兜底值
> 500逻辑过期(异步重建)
> 500降级熔断(直接返回错误/静态兜底页)

考察意图

考察候选人是否理解 Singleflight 在 PHP-FPM 下的不可用边界,能否根据并发量和业务对旧值的容忍度量化选型,而非背"用互斥锁防击穿"的八股答案。

追问链

Q1:互斥锁的 EX(锁超时)应设多大?设置不合理会怎样?

Q2:逻辑过期方案,如何防止多个 Worker 同时触发异步重建?

Q3:逻辑过期方案,重建任务执行失败(如 DB 超时)怎么处理?

Q4:Singleflight 在 Swoole 场景下如何实现?

Q5:大促开始前如何预防热点 Key 击穿?

完整答案/易错点/代码见「学编程拿offer」公众号,回复「缓存击穿」获取

基于 Apache License 2.0 开源