什么是缓存击穿?如何解决?
简化版
缓存击穿指一个热点 key 突然失效,大量并发请求同时发现缓存未命中,于是一起查数据库并重建缓存,把数据库打爆。解决方案是互斥锁/单飞机制让一个线程重建缓存,其他请求等待或返回旧值;也可以用逻辑过期、热点永不过期、后台刷新等方式降低风险。
详细版
缓存击穿的重点是“单个热点 key”。它和雪崩、穿透不同:
| 问题 | 数据是否存在 | 触发点 | 主要方案 |
|---|---|---|---|
| 击穿 | 存在 | 热点 key 过期 | 互斥锁、逻辑过期、热点续期 |
| 雪崩 | 存在 | 大量 key 同时失效 | TTL 随机、高可用、限流降级 |
| 穿透 | 不存在 | 查询不存在数据 | 缓存空值、布隆过滤器 |
常见做法是:缓存未命中后先抢分布式锁,抢到锁的线程查数据库并回填缓存;没抢到锁的线程短暂等待、重试读缓存,或者直接返回旧值。高并发核心热点更适合逻辑过期:缓存不过期或不物理删除,只在值里保存过期时间,过期后由后台或单个线程异步刷新。
完整版教学
一、击穿的现场是什么样
假设首页有一个爆款商品,QPS 很高。这个商品详情缓存过期的一瞬间,几千个请求同时进来,全部发现 Redis 没有值。如果每个请求都去查数据库并回填缓存,数据库会同时承担几千次相同查询,压力瞬间飙升。
这就是击穿:缓存层对热点 key 的保护突然消失,热点流量直接穿过缓存打到 DB。它的本质不是缓存未命中,而是“热点 + 并发重建”。
二、互斥锁方案怎么做
互斥锁方案的核心是只允许一个请求重建缓存。流程是:请求发现缓存未命中,尝试用 Redis SET NX EX 抢锁;抢到锁的请求查数据库、写缓存、释放锁;没抢到锁的请求等待几十毫秒后再读缓存,或者直接返回兜底。
这个方案实现简单,但要注意锁必须设置过期时间,防止重建线程挂掉导致锁不释放;释放锁要校验 value,防止误删别人的锁;等待方不能无限等,否则会把线程池拖死。
三、逻辑过期为什么更适合超热点
逻辑过期不是让 Redis key 真正过期,而是在缓存 value 中保存一个 expireTime。请求读到缓存后,如果发现逻辑时间过期,不立即删除,也不让所有请求查 DB,而是尝试抢锁异步刷新。抢不到锁的请求先返回旧值。
它的好处是高可用:热点 key 不会突然消失,用户请求始终能拿到一个结果。代价是短时间可能返回旧数据,所以适合商品详情、排行榜、配置展示等能容忍短暂旧值的场景,不适合余额、库存扣减这类强约束读写。
四、热点永不过期不是字面上不管
有些热点 key 会设置很长 TTL,甚至不设置物理过期,由后台任务定时刷新。这也能避免击穿。但“永不过期”不等于永远不更新,必须有后台刷新、变更事件删除/刷新、人工兜底和监控,否则缓存会长期陈旧。
如果热点 key 体量不大,可以把热点列表提前识别出来,单独采用更稳的刷新策略;普通 key 继续用常规 TTL 即可。
五、如何选择方案
读请求必须尽量拿到最新值,可以用互斥锁同步重建,牺牲一点延迟。读请求更看重可用性,可以用逻辑过期或返回旧值,牺牲短暂一致性。热点极高且数据变化不频繁,可以做后台刷新和多级缓存。
还要配合限流。即使互斥锁保护 DB,等待锁的请求也可能堆满线程池,所以等待时间、重试次数、兜底策略都要设上限。
六、面试追问与工程边界
面试官常会追问“互斥锁会不会把请求都堵住”。会,所以等待锁的请求必须有超时上限,不能无限阻塞。可以短暂 sleep 后重试读缓存,也可以直接返回旧值或降级结果。热点极高时,逻辑过期通常比同步等待更稳。
还要注意锁粒度。锁粒度应该是具体业务 key,而不是整个缓存类型,否则一个商品重建会阻塞所有商品。重建失败也要释放锁并记录告警,避免热点 key 长时间无法恢复。
七、常见误区与追问
这道题不能只背概念,要把「缓存击穿」放回真实分布式系统里解释:参与方是谁、状态怎么流转、失败后怎么恢复,以及它在一致性、性能、可用性之间做了什么取舍。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | 缓存击穿是热点 key 过期瞬间大量并发请求同时打到数据库,常用互斥锁、逻辑过期和热点永不过期 | 不要停在名词解释 |
| 流程机制 | 热点 key 过期 -> 大量请求同时未命中 -> 只有一个线程拿锁重建缓存 -> 其他线程等待或返回旧值 -> 缓存恢复后请求回到缓存 | 说明触发方、存储方、确认点和兜底 |
| 工程取舍 | 一个热点商品 key 过期时有 5000 个并发请求,若都查库会形成瞬时尖峰 | 缓存提升吞吐但会引入旧值、热点、内存和失效风暴问题 |
缓存击穿 面试拆解:
1. 热点 key 过期
2. 大量请求同时未命中
3. 只有一个线程拿锁重建缓存
4. 其他线程等待或返回旧值
5. 缓存恢复后请求回到缓存
记忆钩子:先说明缓存承担的读写压力,再拆穿透、击穿、雪崩、热点、一致性和淘汰策略;回答时要紧扣「缓存击穿」这道题,不要把相邻概念混成一段泛泛的分布式套话。
- 误区:击穿和穿透一样。 击穿查询的是存在的热点 key,穿透查询的是不存在的数据。
- 误区:加锁后所有问题都没了。 锁要有超时、降级和防死锁,等待线程也要控制。
- 误区:热点 key 永不过期没有代价。 需要主动更新或逻辑过期,否则可能长期返回旧值。
- 追问:逻辑过期怎么做? 缓存值里存 expireAt,过期后先返回旧值,再异步重建。
- 追问:击穿适合什么方案? 热点 key 识别、互斥重建、逻辑过期和本地缓存。
- 追问:如何防止重建失败? 保留旧值、短期降级、重试和告警。
八、加强记忆
缓存击穿是“单个热点 key 过期后,大量并发一起回源”。治理核心是避免所有请求同时重建缓存:互斥锁让一个请求查库,逻辑过期让大部分请求先返回旧值,热点永不过期和后台刷新让 key 不突然消失。击穿看单个热点,雪崩看大面积失效,穿透看不存在数据。