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。核心流程:

  1. 请求到达后,先查缓存(L1/L2),若命中则直接返回。
  2. 若未命中,尝试获取互斥锁。
  3. 获取锁的线程执行回源查询数据库,并更新缓存。
  4. 释放锁,其他等待线程直接读取新缓存。

风险识别:

  • 锁竞争导致吞吐量下降,尤其在单机多线程场景下。
  • 无法跨进程协调,多实例部署时每个实例各自重建缓存,依然会重复回源。
避坑: 本地锁配合主动缓存预热可降低击穿概率,但不能消除。切勿在本地锁内执行长时间 I/O 操作(如远程 RPC),否则会阻塞整个 Worker 线程池。

3.3 热点互斥方案(自适应探测 + 短暂过期不删)

热点互斥是本地锁的升级版:系统通过滑动窗口或统计模块识别热点 Key,当 Key 失效时,不立即删除,而是将其标记为“过期但允许短暂复用”。这样未拿到锁的线程可以读取过期副本,避免空回源。

策略 实现方式 优点 缺点
过期延期 在未命中时返回旧值,同时后台异步更新 用户体验平滑,无等待 数据短暂不一致,需业务容忍
短暂锁定 只对检测出的热点 Key 加互斥锁 资源消耗低,非热点不锁 热点检测算法有误差,可能漏检
架构关键: 热点互斥方案需在 L2 层缓存客户端嵌入保底返回机制:即使 Key 过期,只要未超过可承受的过期时间(如 2 秒),仍允许返回旧数据。代码层面不额外写锁,而是通过SET key value EX ttl NX 实现无锁互斥。

3.4 分布式锁方案(跨节点协调)

当应用为多节点集群时,必须使用分布式锁来保证全局只有一个线程执行回源。常见实现有 Redis RedLock、ZooKeeper 临时顺序节点 以及 Etcd 租约。核心流程:

  1. 查询缓存无果后,尝试在分布式锁服务中加锁(Key 为缓存名称或自定义锁标识)。
  2. 成功获取锁的节点执行数据库查询,写入缓存后释放锁。
  3. 未获取锁的节点短暂自旋(如 50ms),再次读取缓存,超时则降级处理。

一致性保障要点:

  • 锁必须设置自动过期(TTL),防止死锁。
  • 业务线程应在重建完成后主动续期,避免锁过期导致重复重建。
  • 锁的粒度应精确到单个 Key,不可用全局锁。
风险识别: 分布式锁引入额外的网络开销,在 Redis 主从故障时可能丢失锁导致并发重建。推荐使用 RedLock 或 ZooKeeper 强一致性锁,但需权衡延迟。非核心场景可接受概率性重复,采用乐观分布式锁(SETNX + 失败重试)降低复杂度。

3.5 降级回源方案与流程

降级回源是最后一道防线:当锁获取失败、锁服务不可用或数据库超时,系统应返回合理的降级数据,而不是直接报错或击穿。

降级等级 数据来源 适用条件
L0 本地内存中的静态缓存(如配置文件) 分布式缓存及 DB 均不可用
L1 过期副本(热点互斥中的旧数据) 仅 Key 失效但锁竞争激烈
L2 备用缓存节点(如哨兵切换后的从库) 主缓存节点宕机
L3 异步聚合的降级快照(如最近 5 分钟数据) 数据库超时或锁服务异常

数据流与控制流(本章核心 SVG): 以下流程图展示了从请求到达、缓存未命中、互斥锁尝试、分布式锁协调、回源重建到降级回路的完整数据流与控制流。

缓存击穿防护数据流与控制流 请求到达 检查 L1 / L2 缓存 命中 → 直接返回 未命中 → 进入击穿防护 分布式
锁? 尝试获取分布式锁 锁成功 → 回源重建 锁失败 → 自旋等待 / 降级 降级回源(过期副本/静态) 本地互斥锁(单机) 控制流 蓝→请求流 红→异常/降级 绿→成功/命中 回源查询 DB

3.6 选型权衡矩阵

方案 跨节点协调 延迟影响 实现复杂度 数据一致性 推荐场景
本地互斥锁 否 低(单机) 低 强(进程内) 单实例 / 预发布环境
热点互斥(过期复用) 否 极低 中 弱(允许短暂过期数据) 读多写少、容忍秒级不一致
分布式锁(Redis RedLock) 是 中(+2~5ms) 高 强(主从故障后可能降级) 高一致性要求、集群部署
分布式锁(ZooKeeper) 是 较高(+10~20ms) 高 强(强一致) 金融、交易类关键业务
降级回源(过期副本) 否 无额外延迟 中 弱 任何场景作为兜底

3.7 落地实施步骤

  1. 热点识别阶段: 接入层记录 Key 访问频率,设置阈值(如 QPS > 1000),纳入热点防护名单。
  2. 锁策略配置: 根据集群规模选择分布式锁实现,在配置中心声明锁超时时间(推荐 500ms)、重试次数(3 次)及失败降级动作。
  3. 缓存重建逻辑改造: 将回源代码包裹在 tryLock 块内,更新缓存后设置随机过期偏移(防雪崩)。
  4. 降级数据准备: 预置降级 JSON 或静态文件,或从历史快照加载到本地缓存。
  5. 验收标准:
    • 单热点 Key 失效时,数据库瞬时 QPS ≤ 1 (分布式锁生效)或 ≤ 本地实例数(本地锁生效)。
    • 锁服务不可用(模拟宕机)时,降级数据在 5ms 内返回,不触发数据库连接。
    • 并发压测 1000 线程,缓存击穿导致的错误率 < 0.01%。
关键指标: 落地后应监控缓存重建并发数和锁争抢失败率。推荐使用 Prometheus + Grafana 聚合分布式锁的加锁耗时与续期次数,作为健康度判据。

3.8 风险点识别与规避

风险 触发条件 规避措施
死锁/锁不释放 业务线程异常退出未释放锁 锁设置 TTL(建议锁 TTL = 正常重建耗时 × 1.5)
锁重入导致递归 缓存重建过程中又触发了同 Key 的缓存查询 锁内通过 ThreadLocal 标记不可重入,或使用显示递归锁
锁粒度过大 使用全局锁而非 Key 级别锁 每个 Key 分配唯一锁 ID,避免锁竞争扩散
降级数据与源数据偏差过大 长时间不更新降级快照 降级数据定期刷新(如 1 分钟),并标记过期时间戳,超过阈值返回错误码