缓存击穿

那次惊魂时刻

周三上午 10 点,流量高峰期。

一切看似正常:

  • API 响应时间:50ms
  • 缓存命中率:98%
  • 外部 API 调用成功率:99%

突然:

Inbox
From: 监控系统
To: 运维值班
Time: 周三 10:00
Subject: [P1] Weather API 异常告警
  • 警告:API 响应时间飙升到 8 秒
  • 警告:外部 API 调用失败率飙升
  • 警告:请求超时率 60%

系统差点崩溃。事后分析日志,我发现了一个致命问题。

什么是缓存击穿?

缓存击穿流程
热点 Key 形成
北京 50,000 次/天
上海 40,000 次/天
深圳 30,000 次/天
击穿发生过程
T0
热点 key 过期 weather:北京 缓存失效
T1
100 个并发请求同时到达 全部访问同一个 key
T2
缓存未命中 Redis 中无此数据
T3
100 个请求同时访问外部 API 触发 API 限流
100%
T4
系统崩溃 API 超时,错误率飙升
正常情况 vs 击穿时刻
正常情况
请求
缓存命中
返回 (50ms)
缓存命中率: 98%
击穿时刻
100 请求
缓存过期
外部 API
API 限流状态: 已触发

缓存击穿是指:

  • 某个热点 key(被频繁访问的数据)突然过期
  • 大量并发请求同时访问这个 key
  • 所有请求都穿透到外部 API
  • 外部 API 瞬间压力激增,可能触发限流

关键特征

  • 只有一个 key 过期(不是大面积)
  • 但这个 key 非常”热”(访问量巨大)
  • 并发请求同时击穿缓存层

击穿 vs 穿透

这两个概念很容易混淆,我们来对比一下:

对比项缓存穿透缓存击穿
攻击目标不存在的数据存在的热点数据
key 特征大量不同的无效 key单个有效的热点 key
触发原因恶意攻击/非法请求热点 key 自然过期
数据存在性缓存和外部 API 都没有外部 API 有,缓存刚好过期
请求特点持续的恶意请求并发量大的正常请求

简单记忆

  • 穿透 = 穿透空对象(数据不存在)
  • 击穿 = 击穿热点 key(数据存在但缓存过期)

解决方案一:互斥锁

请求 1
请求 2
请求 3
...
请求 N
互斥锁竞争 (SETNX)
请求 1
获得锁
查缓存 (无) 查数据库 写缓存
释放锁
请求 2
等待锁
sleep(100ms) 重试→查缓存 (命中!)
请求 3
等待锁
sleep(100ms) 重试→查缓存 (命中!)
数据库受到保护 只有一个请求访问数据库
后续请求快速响应 直接从缓存读取数据

核心思路

  • 只让一个请求去调用外部 API
  • 其他请求等待
  • 等第一个请求把数据写入缓存后,大家都从缓存读

优点

  • 保证只有一个请求调用外部 API
  • 完全解决击穿问题
  • 实现相对简单

缺点

  • 需要处理锁的获取和释放
  • 如果持锁请求失败,需要超时机制
  • 性能略有损耗(等待时间)

解决方案二:逻辑过期

写入缓存 数据 + 逻辑过期时间
{ "data": {...}, "expire_at": "2025-03-15T15:00:00" }
用户请求到达 查询缓存数据
检查逻辑过期时间
未过期
直接返回数据
响应快 (50ms)
已过期
立即返回旧数据
用户无感知
后台异步更新
不阻塞用户请求
后台异步更新流程
1 启动异步线程
2 获取更新锁
3 查数据库
4 更新缓存
用户零等待 总是立即返回数据
可能返回旧数据 短暂不一致窗口

优点

  • 用户永远能读到数据(不会等待)
  • 完全避免外部 API 压力
  • 适合对实时性要求不高的场景

缺点

  • 可能返回过期数据(一致性弱)
  • 实现复杂(需要异步线程)
  • 需要处理并发更新问题

方案对比

对比项互斥锁逻辑过期
实现难度⭐⭐ 中等⭐⭐⭐⭐ 复杂
数据一致性强一致弱一致(可能返回旧数据)
响应时间需要等待(~100ms)无需等待(立即返回)
外部 API 压力完全保护完全保护
适用场景大多数场景高并发、可接受短暂不一致

效果验证

实施互斥锁方案后的系统指标:

指标无防护互斥锁方案逻辑过期方案
外部 API 峰值调用量10000 次/秒100 次/秒50 次/秒
平均响应时间2000ms150ms50ms
最大响应时间8000ms300ms100ms
请求超时率60%0.1%0%
数据一致性强一致强一致弱一致

结论

  • 互斥锁方案:适合大多数场景,一致性好
  • 逻辑过期方案:适合超高并发、可接受短暂不一致的场景

当前技术架构

练习

练习 1

缓存击穿和缓存穿透的区别是什么?

参考答案 (3 个标签)
缓存击穿 缓存穿透 概念辨析

答案

对比项缓存穿透缓存击穿
攻击目标不存在的数据存在的热点数据
key 数量大量不同的 key单个热点 key
数据存在性缓存和外部 API 都没有外部 API 有,缓存刚好过期
触发原因恶意攻击/非法请求热点 key 自然过期
并发特征持续的恶意请求高并发的正常请求

简单记忆

  • 穿透 = 数据不存在,请求穿透空对象
  • 击穿 = 热点 key 过期,请求击穿缓存层

练习 2

互斥锁方案中,为什么获取锁后要”双重检查”缓存?

参考答案 (3 个标签)
缓存击穿 互斥锁 双重检查

答案

双重检查的目的是避免重复调用外部 API

场景说明

时间线:
T1: 请求 A 获取锁成功
T2: 请求 B 获取锁失败,进入等待
T3: 请求 A 调用外部 API 并写入缓存,释放锁
T4: 请求 B 获得锁(因为 A 释放了)
T5: 请求 B 再次调用外部 API ← 这是浪费的!

双重检查流程

设计流程
练习 2
  1. 步骤 1:按本节策略读取、写入缓存并处理过期或失效
  2. 步骤 2:校验身份、密钥或权限
  3. 步骤 3:识别请求 key、缓存命中状态和回源数据对象
  4. 步骤 4:根据命中率、数据新鲜度和回源压力调整策略
关注点:命中率、回源压力、TTL 和异常 key 处理。

效果

  • 避免在等待锁期间,其他请求已经写入了数据
  • 减少不必要的外部 API 调用

练习 3

请设计一个带有缓存击穿防护的商品查询流程,使用互斥刷新方案。

参考答案 (3 个标签)
缓存击穿 互斥锁 方案设计

参考答案

设计流程
练习 3
  1. 步骤 1:写入缓存值、空值标记或热点保护状态
  2. 步骤 2:按命中、未命中和异常 key 处理读取与回源
  3. 步骤 3:按本节策略读取、写入缓存并处理过期或失效
  4. 步骤 4:校验身份、密钥或权限
关注点:命中率、回源压力、TTL 和异常 key 处理。

关键点

  1. 互斥锁保护外部 API 调用
  2. 双重检查避免重复调用
  3. 空值缓存防止穿透
  4. 等待重试机制处理锁竞争
  5. finally 块确保锁释放