缓存雪崩/击穿/穿透到底怎么防

聊聊缓存三大经典问题:雪崩是一片缓存同时失效,击穿是一个热点key失效被打爆,穿透是查询压根不存在的数据。挨个说清楚产生原因、真实危害和常见处理方案,帮你分清这三个概念别再混着用。

缓存雪崩击穿穿透到底怎么防

实话第一次听的时候我自己都容易搞混,感觉都是“缓存失效然后数据库被打爆”,好像没啥本质区别。

后来自己实际踩过坑才发现,这三个问题的产生原因完全不同,处理方式也不能一套方案打天下。

今天我们不写代码,就把这三个概念掰开揉碎,讲清楚各自是怎么产生的、真实危害有多大、业界通常怎么应对。

为什么要有缓存

我们平时用 Redis 做缓存,核心逻辑很简单:查询数据先看缓存有没有,有就直接返回,没有才去查数据库,查到了再回填进缓存。

这样能扛住绝大部分请求,数据库只需要应付缓存没命中的那一小部分流量。

打个比方,缓存就像单位门口的快递代收点,大部分包裹取件码一扫就能拿走,只有代收点没有的包裹才需要跑一趟仓库(数据库)。

问题就出在:如果代收点突然瘫痪,或者某个爆款商品的取件码集中失效,所有人都得跑仓库,仓库直接被挤爆。

这就是缓存雪崩、击穿、穿透要解决的三种不同的“挤爆仓库”场景。

缓存穿透:查的东西压根不存在

产生原因

穿透说的是:查询的数据在缓存里没有,在数据库里也没有。

既然数据库里都没有,缓存自然也不会有这条数据,那每次查询这个不存在的 key,都会直接跳过缓存,一路打到数据库上。

典型场景:恶意攻击者故意构造一堆不存在的用户 ID、订单号去查询接口,或者业务方自己传了个错误的 ID。

危害

单次穿透其实危害不大,数据库扛一下就过去了。

真正麻烦的是被恶意攻击时,攻击者会用大量不存在的 key 高频请求,因为每次都绕过缓存直击数据库,短时间内就能把数据库连接池打满,拖垮整个系统,属于一种低成本高杠杆的攻击方式。

处理建议

  • 缓存空值:查询数据库发现数据不存在,也往缓存里存一个空标记(值可以是一个约定的空对象或者特殊字符串),设置一个比较短的过期时间。下次同样的查询直接命中这个空值缓存,不用再打数据库。
  • 布隆过滤器(Bloom Filter):在缓存之前加一层布隆过滤器,提前把所有可能存在的 key 都放进去。查询时先问一下布隆过滤器这个 key 有没有可能存在,如果过滤器说“肯定不存在”,直接拒绝,连缓存都不用查。布隆过滤器的特点是有一定误判率(可能把不存在的判断成存在),但不会漏判(存在的一定不会被判断成不存在),所以能拦掉绝大部分无效查询。
  • 接口层做基础校验:比如 ID 格式明显不对、超出合理范围的,直接在接口层拦掉,不进入后续查询流程。

缓存击穿:一个热点 key 突然失效

产生原因

击穿说的是:某一个访问量特别高的热点 key,缓存过期失效的那一瞬间,海量并发请求同时穿过缓存,一起打到数据库上。

跟穿透的区别在于:击穿的数据在数据库里是真实存在的,只是缓存刚好失效了,而且失效的是一个被高频访问的热点。

打个比方,就像某个爆款商品的取件码突然过期重置,正好这个商品今天搞大促,成千上万人同时来取,代收点还没来得及重新生成取件码,所有人瞬间全部涌向仓库。

危害

跟穿透比,击穿的杀伤力更集中:因为是热点数据,并发量通常非常大,短时间内数据库可能被同一条 SQL 的重复查询请求瞬间打满连接数,造成响应变慢甚至服务雪崩式连锁故障。

处理建议

  • 加互斥锁(Mutex Lock):缓存失效时,只允许第一个请求去查数据库并重建缓存,其他请求原地等待或者重试,等缓存重建完之后大家都从缓存里取,不会全部涌向数据库。这个锁一般用 Redis 的 SETNX 之类的机制实现,粒度是具体的 key。
  • 热点数据不设过期时间,用后台异步更新:对于明确知道是热点的数据(比如首页推荐、爆款商品详情),干脆不设过期时间,靠后台任务定时刷新缓存内容,避免出现“过期瞬间”这个窗口期。
  • 提前预热:可预知的热点(比如大促活动开始前),提前把数据加载进缓存,错峰处理,不要等第一个用户请求来触发缓存重建。

缓存雪崩:一大片缓存同时失效

产生原因

雪崩说的是:大量不同的 key 在同一时间集中失效,或者 Redis 服务本身直接宕机、不可用了,导致所有请求一下子全部涌向数据库。

常见的触发场景有两种:

  • 批量设置缓存的时候,给了相同或者相近的过期时间,到了那个时间点,一大片 key 集中过期。
  • Redis 实例本身挂了(宕机、网络分区、主从切换未完成),所有缓存查询直接失效,等于缓存整体“罢工”。

打个比方,这次不是一个爆款取件码过期,而是整个代收点突然关门,所有取件请求全部涌向仓库,仓库直接崩溃。

危害

雪崩是三者里波及面最大的:不是一个热点 key 的问题,而是大范围甚至全局性的缓存失效,数据库瞬间承受平时几倍甚至几十倍的压力,很容易被直接打垮,进而引发整个系统的连锁故障(数据库崩了,依赖数据库的其他服务跟着崩)。

处理建议

  • 过期时间加随机值:批量设置缓存时,别用同一个固定过期时间,在基础时间上加一个随机偏移量(比如基础 30 分钟 + 随机 0~5 分钟),让缓存失效的时间点分散开,不要集中在同一时刻。
  • Redis 高可用架构:用哨兵(Sentinel)或者集群(Cluster)模式部署,避免单点故障导致整个缓存服务不可用。
  • 多级缓存兜底:在 Redis 之上再加一层本地缓存(比如应用内存缓存),Redis 不可用的时候本地缓存还能顶一阵子,给恢复留出时间窗口。
  • 服务降级和限流:数据库压力过大时,接口层做限流(拒绝超出承受范围的请求)或者降级(返回默认值/兜底数据,而不是硬查数据库),保护数据库不被彻底打垮。
  • 数据库层加保护:数据库连接池设置合理上限、加慢查询监控,即便真的被打满,也能尽快发现问题,而不是等到服务整体挂掉才反应过来。

三者放一起对比着记

口头总结一遍,方便脑子里过一遍区分:

  • 穿透:查的数据压根不存在,缓存和数据库都没有,每次都直击数据库,通常是恶意攻击或者参数错误导致。
  • 击穿:查的数据是真实存在的热点,只是缓存刚好失效那一瞬间,海量并发一起打数据库,通常是热点 key 过期触发。
  • 雪崩:大范围缓存同时失效或者缓存服务本身宕机,波及面最大,通常是批量设置了相近过期时间,或者 Redis 本身出故障。

记忆的角度可以想成:穿透是“查不到的东西一直被查”,击穿是“热点数据突然没了”,雪崩是“整个缓存系统集体罢工”。

小结

这三个问题看起来都是“缓存失效导致数据库压力暴增”,但根源、波及范围、处理思路完全不一样。

穿透靠布隆过滤器和空值缓存挡在最外层;击穿靠互斥锁和热点数据不过期来化解并发冲击;雪崩靠打散过期时间、多级缓存和高可用架构来兜底。

实际生产系统里,这三种防护手段基本是组合使用的,不会只选一种,毕竟谁也说不准线上会先撞到哪个坑。

PS:不知道为什么,很多资料把这三个概念的边界讲得很模糊,导致我自己一开始也分不清击穿和穿透。希望这次拆开讲清楚原因之后,你也能一次记住不会再搞混。

那缓存这三大经典问题就先讲到这里,日常做缓存设计的时候,建议对照这三种场景挨个想一遍防护措施有没有落地。

更多推荐

章节目录