什么是缓存击穿?如何解决?
简化版
缓存击穿指某个热点 key(大量请求访问的存在的数据)在缓存中突然过期失效的瞬间,大量并发请求同时未命中缓存、一起压向数据库,可能把 DB 打垮。它和穿透的区别是:数据是存在的,只是 key 刚好过期了。两个主流解法:① 互斥锁——只让一个线程去查 DB 重建缓存,其他线程等待或重试;② 逻辑过期——缓存永不物理过期,用字段标记逻辑过期时间,过期后异步重建、期间返回旧值。
详细版
问题本质:一个被高并发访问的热点 key 过期的那一瞬间,成百上千请求同时穿过缓存打到 DB。
解法一:互斥锁(保证只有一个请求重建缓存)
Object getData(String key) {
Object value = redis.get(key);
if (value != null) return value; // 命中
// 未命中,尝试获取锁(只有一个线程能拿到)
if (redis.setnx(lockKey, 1, 10)) { // 分布式锁
try {
value = db.query(key); // 只有拿到锁的线程查 DB
redis.set(key, value, 3600); // 重建缓存
} finally {
redis.del(lockKey); // 释放锁
}
} else {
Thread.sleep(50); // 没拿到锁,短暂等待后重试
return getData(key);
}
return value;
}
解法二:逻辑过期(缓存不设 TTL,靠字段判断)
// 缓存的 value 里带一个 expireTime 字段,key 本身永不过期
Object getData(String key) {
CacheData data = redis.get(key);
if (data == null) return null; // 理论上热点 key 一直在
if (data.expireTime > now()) return data.value; // 逻辑未过期,直接返回
// 逻辑已过期:拿到锁的线程开异步任务重建,当前直接返回旧值
if (redis.setnx(lockKey, 1, 10)) {
asyncRebuild(key); // 异步重建
}
return data.value; // 无论是否重建,都先返回旧值
}
- 互斥锁:保证只有一个请求打 DB,其他等待。强一致(返回的是最新值),但等待期间有阻塞。
- 逻辑过期:永不阻塞、可用性高,但过期后一段时间返回的是旧数据(最终一致)。
- 另可给热点 key 设永不过期(后台定时刷新)。
完整版教学
一、击穿 vs 穿透 vs 雪崩:先分清
- 击穿(本题):单个热点 key 存在,但刚好过期失效的瞬间,海量请求压向 DB。特点是「一个热点 key、失效瞬间」。
- 穿透:查的数据根本不存在,缓存永远挡不住。
- 雪崩:大量 key 同时失效或缓存整体宕机,DB 被压垮。特点是「大批 key、同时失效」。
击穿的关键词是**「热点 + 失效瞬间」**——数据本身是存在且被高频访问的,问题出在它过期的一刹那,缓存出现了短暂空窗,所有请求一拥而上。比如一个爆款商品详情,QPS 上万,缓存过期的那一秒,上万请求全部落到 DB。
二、解法一:互斥锁——只放一个进去查 DB
核心思路:既然问题是「过期瞬间大量请求同时查 DB」,那就限制只让一个请求去查 DB 重建缓存,其余请求等它建好。
实现:请求未命中缓存后,先去抢一把互斥锁(单机用 synchronized/Lock,分布式用 Redis 的 SETNX 分布式锁):
- 抢到锁的线程:去查 DB、重建缓存、然后释放锁。
- 没抢到锁的线程:说明已有线程在重建,短暂等待(sleep 几十毫秒)后重试读缓存——此时缓存大概率已被重建好,直接命中。
这样无论多少并发,同一时刻只有一个请求真正打到 DB,其他都被挡在锁外。
优点:返回的是最新数据,强一致。 缺点:没抢到锁的线程要等待/重试,有一定阻塞和延迟;锁本身要处理好超时释放(防止持锁线程宕机导致死锁)、误删(用唯一 value 标识 + Lua 删除)等问题。
三、解法二:逻辑过期——永不真过期,异步重建
另一种思路彻底避免「缓存空窗」:让热点 key 在 Redis 里永不设置物理 TTL(永不真正过期消失),而是在 value 内部存一个 expireTime 逻辑过期时间字段。
读的时候:
- 缓存里一直有数据(不会 miss);
- 检查 value 里的
expireTime:没到逻辑过期时间 → 直接返回;已过逻辑过期时间 → 说明该刷新了,此时抢锁,抢到的线程开一个异步任务去重建缓存,而当前请求(以及重建期间的其他请求)直接返回旧的 value。
这样任何请求都不会阻塞、不会打到 DB(重建是异步的),可用性极高。代价是逻辑过期后、异步重建完成前,返回的是旧数据(牺牲了一致性,只保证最终一致)。
四、两种方案的本质权衡:一致性 vs 可用性
| 互斥锁 | 逻辑过期 | |
|---|---|---|
| 一致性 | 强(返回最新值) | 弱(过期后返回旧值) |
| 可用性 | 有等待/阻塞 | 无阻塞,始终有值返回 |
| 复杂度 | 中(锁的管理) | 高(额外字段 + 异步重建) |
| DB 压力 | 低(只一个请求) | 极低(异步、非阻塞) |
| 适用 | 要求数据准确 | 要求高可用、能容忍旧值 |
这正是 CAP 权衡在缓存层的体现:互斥锁偏一致性(CP 味),逻辑过期偏可用性(AP 味)。选哪个看业务——金额、库存这类要准的用互斥锁;商品详情、排行榜这类能容忍短暂旧值的用逻辑过期。
五、更简单的兜底:热点 key 永不过期 + 定时刷新
对于已知的、固定的热点数据(如首页、热门榜单),可以干脆不设过期时间,由一个后台定时任务主动刷新缓存。这样根本不存在「过期瞬间」,也就没有击穿。适合热点可预测的场景;对热点不可预测(临时爆红)的场景,还得靠互斥锁/逻辑过期动态应对。
六、常见误区与追问
| 考点 | 正确口径 |
|---|---|
| 缓存击穿 | 单个热点 key 过期,瞬时并发打到数据库 |
| 典型场景 | 爆款商品、明星直播间、热点配置 |
| 核心手段 | 互斥锁、逻辑过期、热点永不过期、异步刷新 |
hot key expires
10000 requests miss cache
only 1 request rebuilds cache with mutex
others wait, retry, or return stale value
击穿的关键词是“热点 key + 过期瞬间 + 高并发回源”。
- 误区:缓存击穿和缓存穿透一样。 击穿访问的是存在的热点数据,只是缓存失效;穿透访问的是不存在或非法数据。
- 误区:给热点 key 加互斥锁就没有代价。 锁会增加等待和超时风险,要设置过期、降级和失败兜底。
- 误区:热点 key 永不过期就完美。 永不过期要配合异步刷新和版本控制,否则可能长期返回旧数据。
- 追问:逻辑过期怎么工作? 缓存值里存过期时间,过期后先返回旧值,同时后台异步重建。
- 追问:互斥锁应该锁什么粒度? 通常按 key 加锁,避免不同 key 相互阻塞。
- 追问:热点 key 如何发现? 可通过 Redis hotkeys、访问日志、业务埋点、代理层统计和突增流量监控发现。
七、加强记忆
缓存击穿 = 单个热点 key(数据存在)在缓存过期的瞬间,海量并发一起打到 DB。区别于穿透(数据不存在)、雪崩(大量 key 同时失效)。两大解法:① 互斥锁——只让抢到锁的一个线程查 DB 重建,其余等待重试,强一致但有阻塞;② 逻辑过期——key 永不物理过期、value 里存逻辑过期时间,过期后异步重建、期间返回旧值,高可用但返回旧数据(最终一致)。本质是一致性 vs 可用性的权衡。对可预测的热点还可永不过期 + 定时刷新。记住击穿关键词:热点 key、失效瞬间、只放一个进去。