3. 缓存击穿防护策略:互斥锁、热点互斥、分布式锁方案与降级回源
缓存击穿是指流量集中攻击一个已失效的热点 Key,在缓存重建完成前,大量请求同时穿透至数据库,导致后端负载飙升甚至宕机。与缓存雪崩不同,击穿只针对单个热点 Key,其防护核心在于控制并发重建的临界区。本章从互斥锁、热点互斥、分布式锁及降级回源四个维度,分析各方案在分层架构中的职责边界与选型权衡。
3.1 问题边界与分层职责
在缓存分层架构中,缓存击穿发生在本地缓存层(L1)与分布式缓存层(L2)之间,或 L2 失效后穿透至数据库层(L3) 的路径上。各层职责如下:
| 层级 | 职责 | 与击穿防护的关联 |
|---|---|---|
| L1 本地缓存 | 存放最热 Key,TTL 短,用于快速响应 | 本地锁可减少单机重复重建,但跨节点无效 |
| L2 分布式缓存 | 存放全量热点 Key,TTL 较长 | 分布式锁在此层协调,防止多节点同时回源 |
| L3 数据库 | 持久化存储,是最终一致性源头 | 降级回源策略保护数据库不被击穿流量冲垮 |
| L0 客户端/网关 | 流量整形与熔断 | 热点探测与限流,可作为前置缓冲 |
架构提示: 缓存击穿防护方案应部署在 L2 与 L3 之间,并同步在 L1 层做本地互斥兜底。切勿将分布式锁下放到客户端网关层,否则会增加网络延迟与一致性问题。
3.2 互斥锁方案(本地锁)
适用场景: 单机部署或同一进程内的缓存重建。常见方案为 synchronized、ReentrantLock 或 Semaphore。核心流程:
- 请求到达后,先查缓存(L1/L2),若命中则直接返回。
- 若未命中,尝试获取互斥锁。
- 获取锁的线程执行回源查询数据库,并更新缓存。
- 释放锁,其他等待线程直接读取新缓存。
风险识别:
- 锁竞争导致吞吐量下降,尤其在单机多线程场景下。
- 无法跨进程协调,多实例部署时每个实例各自重建缓存,依然会重复回源。
避坑: 本地锁配合主动缓存预热可降低击穿概率,但不能消除。切勿在本地锁内执行长时间 I/O 操作(如远程 RPC),否则会阻塞整个 Worker 线程池。
3.3 热点互斥方案(自适应探测 + 短暂过期不删)
热点互斥是本地锁的升级版:系统通过滑动窗口或统计模块识别热点 Key,当 Key 失效时,不立即删除,而是将其标记为“过期但允许短暂复用”。这样未拿到锁的线程可以读取过期副本,避免空回源。
| 策略 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 过期延期 | 在未命中时返回旧值,同时后台异步更新 | 用户体验平滑,无等待 | 数据短暂不一致,需业务容忍 |
| 短暂锁定 | 只对检测出的热点 Key 加互斥锁 | 资源消耗低,非热点不锁 | 热点检测算法有误差,可能漏检 |
架构关键: 热点互斥方案需在 L2 层缓存客户端嵌入保底返回机制:即使 Key 过期,只要未超过可承受的过期时间(如 2 秒),仍允许返回旧数据。代码层面不额外写锁,而是通过
SET key value EX ttl NX 实现无锁互斥。
3.4 分布式锁方案(跨节点协调)
当应用为多节点集群时,必须使用分布式锁来保证全局只有一个线程执行回源。常见实现有 Redis RedLock、ZooKeeper 临时顺序节点 以及 Etcd 租约。核心流程:
- 查询缓存无果后,尝试在分布式锁服务中加锁(Key 为缓存名称或自定义锁标识)。
- 成功获取锁的节点执行数据库查询,写入缓存后释放锁。
- 未获取锁的节点短暂自旋(如 50ms),再次读取缓存,超时则降级处理。
一致性保障要点:
- 锁必须设置自动过期(TTL),防止死锁。
- 业务线程应在重建完成后主动续期,避免锁过期导致重复重建。
- 锁的粒度应精确到单个 Key,不可用全局锁。
风险识别: 分布式锁引入额外的网络开销,在 Redis 主从故障时可能丢失锁导致并发重建。推荐使用
RedLock 或 ZooKeeper 强一致性锁,但需权衡延迟。非核心场景可接受概率性重复,采用乐观分布式锁(SETNX + 失败重试)降低复杂度。
3.5 降级回源方案与流程
降级回源是最后一道防线:当锁获取失败、锁服务不可用或数据库超时,系统应返回合理的降级数据,而不是直接报错或击穿。
| 降级等级 | 数据来源 | 适用条件 |
|---|---|---|
| L0 | 本地内存中的静态缓存(如配置文件) | 分布式缓存及 DB 均不可用 |
| L1 | 过期副本(热点互斥中的旧数据) | 仅 Key 失效但锁竞争激烈 |
| L2 | 备用缓存节点(如哨兵切换后的从库) | 主缓存节点宕机 |
| L3 | 异步聚合的降级快照(如最近 5 分钟数据) | 数据库超时或锁服务异常 |
数据流与控制流(本章核心 SVG): 以下流程图展示了从请求到达、缓存未命中、互斥锁尝试、分布式锁协调、回源重建到降级回路的完整数据流与控制流。
3.6 选型权衡矩阵
| 方案 | 跨节点协调 | 延迟影响 | 实现复杂度 | 数据一致性 | 推荐场景 |
|---|---|---|---|---|---|
| 本地互斥锁 | 否 | 低(单机) | 低 | 强(进程内) | 单实例 / 预发布环境 |
| 热点互斥(过期复用) | 否 | 极低 | 中 | 弱(允许短暂过期数据) | 读多写少、容忍秒级不一致 |
| 分布式锁(Redis RedLock) | 是 | 中(+2~5ms) | 高 | 强(主从故障后可能降级) | 高一致性要求、集群部署 |
| 分布式锁(ZooKeeper) | 是 | 较高(+10~20ms) | 高 | 强(强一致) | 金融、交易类关键业务 |
| 降级回源(过期副本) | 否 | 无额外延迟 | 中 | 弱 | 任何场景作为兜底 |
3.7 落地实施步骤
- 热点识别阶段: 接入层记录
Key访问频率,设置阈值(如 QPS > 1000),纳入热点防护名单。 - 锁策略配置: 根据集群规模选择分布式锁实现,在配置中心声明锁超时时间(推荐 500ms)、重试次数(3 次)及失败降级动作。
- 缓存重建逻辑改造: 将回源代码包裹在
tryLock块内,更新缓存后设置随机过期偏移(防雪崩)。 - 降级数据准备: 预置降级 JSON 或静态文件,或从历史快照加载到本地缓存。
- 验收标准:
- 单热点 Key 失效时,数据库瞬时 QPS ≤ 1 (分布式锁生效)或 ≤ 本地实例数(本地锁生效)。
- 锁服务不可用(模拟宕机)时,降级数据在 5ms 内返回,不触发数据库连接。
- 并发压测 1000 线程,缓存击穿导致的错误率 < 0.01%。
关键指标: 落地后应监控
缓存重建并发数和锁争抢失败率。推荐使用 Prometheus + Grafana 聚合分布式锁的加锁耗时与续期次数,作为健康度判据。
3.8 风险点识别与规避
| 风险 | 触发条件 | 规避措施 |
|---|---|---|
| 死锁/锁不释放 | 业务线程异常退出未释放锁 | 锁设置 TTL(建议锁 TTL = 正常重建耗时 × 1.5) |
| 锁重入导致递归 | 缓存重建过程中又触发了同 Key 的缓存查询 | 锁内通过 ThreadLocal 标记不可重入,或使用显示递归锁 |
| 锁粒度过大 | 使用全局锁而非 Key 级别锁 | 每个 Key 分配唯一锁 ID,避免锁竞争扩散 |
| 降级数据与源数据偏差过大 | 长时间不更新降级快照 | 降级数据定期刷新(如 1 分钟),并标记过期时间戳,超过阈值返回错误码 |