先说结论:
缓存击穿(Cache Breakdown)指的是——某个热点 key 刚好在过期的那一瞬间,被海量并发请求同时打中,缓存里没数据,请求全部砸向数据库,数据库瞬间被压垮。
说白了,它跟"缓存里一直没数据"不是一回事,而是"本来该有的数据,恰好在那一刻失效了"。
那问题来了——平时风平浪静,为什么偏偏过期那一刻就崩?
举个通俗一点的例子。
假设我们按文章 ID 去查文章内容,正常链路是:
先读 Redis,命中就直接返回;
没命中才回源数据库,再把结果写回缓存。
可要是这个 key 刚刚过期,同一毫秒里涌进来几万个请求,它们一查 Redis 全是空,于是几万个请求同时往数据库冲。
数据库的并发能力远低于 Redis,很容易在瞬时高并发下被打满。

可以看到,请求原本都应该命中 Redis,但由于热点 Key 恰好过期,所有请求瞬间涌向数据库。
有人可能会问:
查询的时候不是已经把数据同步进 Redis 了吗,怎么还会压垮库?
关键在"重建缓存"这一步。
从数据库把数据查出来、再写回 Redis,中间要花时间。
从数据库把数据查出来,再写回 Redis,这个过程不是瞬间完成的。
要是涉及多表 Join、聚合统计等复杂查询,重建缓存往往需要几十毫秒,甚至更长时间。
只要这段时间内并发足够高,数据库仍然可能被大量请求压垮。
这种"过期瞬间被并发打穿"的现象,就叫缓存击穿。
那怎么治?
主流就两条路:
一是加互斥锁(本质就是分布式锁)。
二是逻辑过期。
下面把两种方案拆开讲,顺便说清它们的取舍。
方案一:互斥锁(分布式锁)
一句话:谁先抢到锁,谁去重建缓存,其他人排队等。
流程是这样的。
线程 A 来查缓存,发现是空的,于是加一把互斥锁(也就是分布式锁),然后去查数据库、重建缓存、把数据写回 Redis,最后释放锁。

与此同时线程 B 也来查,缓存还是空的(因为 A 还没建完)。
B 也想拿锁,但锁在 A 手里,于是 B 拿锁失败,只能先睡一会儿,再重新尝试。
等 A 把数据写回 Redis、释放锁之后,B 再查缓存,就能直接拿到数据。
核心点:
同一时刻只有一个线程能拿到锁,其余线程全在等待。
好处是数据强一致,坏处是性能偏低——大家都得排队。
方案二:逻辑过期
这个思路和互斥锁不一样,它根本不给热点 key 设物理过期时间。
那怎么知道数据老了没?
答案是:
存数据的时候,额外塞一个 expire 字段进去,用这个字段判断逻辑上过没过期。
看下它的存储结构:
key 还是文章 ID,value 里 id、title 是正常业务字段,我们多加了一个 expire 字段,专门记录这条数据的逻辑过期时间。

具体流程是这样的。
线程 A 查缓存,发现这条数据"逻辑上"已经过期了,于是去读 expire 字段确认——确实过期。
既然过期了,就得重建缓存。
所以 A 先去抢一把互斥锁(注意,这里同样需要加锁,否则多个线程都会同时开启后台重建任务,数据库依然可能被打爆)。
不过和方案一不同的是:
A 抢到锁之后,会异步开启一个后台线程负责重建缓存(查库 → 写回缓存 → 重置 expire → 释放锁)。

重点来了:
线程 A 自己不等待新线程建完,直接把当前这份(已过期的)旧数据返回给用户。
接着线程 C 来了,也发现数据过期,也想拿锁重建。
但锁还在 A 手里,C 拿锁失败——C 也不等待,直接返回旧数据。
如果 C 在这儿死等锁,那不就和互斥锁一模一样了?
所以逻辑过期的精髓就是:
拿不到锁的线程,直接返回旧数据,绝不阻塞。
等重建线程把新数据写回缓存、释放锁之后,再来线程 D,查到的就是最新的、没过期的数据了。
两种方案怎么选?
一句话:
互斥锁保一致性,逻辑过期保可用性。
| 互斥锁 | 强一致(其他线程等待,数据一定最新) | 较低(线程互相等待) | 对一致性要求高,如跟钱相关的业务 |
| 逻辑过期 | 不保证绝对一致(可能返回旧数据) | 较高(不阻塞,直接返回) | 更看重用户体验,如互联网高并发读多场景 |
怎么选?
看业务底线:
如果你的数据必须强一致,比如订单、余额这类跟钱打交道的地方,闭眼选互斥锁。
如果你的业务更在意用户体验,宁可先返回旧数据也不能让用户卡住,那逻辑过期更香。
网硕互联帮助中心


评论前必须登录!
注册