2047 字
约 6 分钟
0
缓存设计:缓存雪崩、缓存穿透、缓存击穿

与访问磁盘上的数据相比,访问内存中的数据要快得多,因此内存级缓存可以极大地提升系统的响应速度和吞吐量。但缓存不是银弹,用不好反而会把数据库"打挂"。本篇文章就来系统梳理缓存领域的三大经典问题——缓存雪崩、缓存穿透、缓存击穿,以及各自的应对方案。

一、为什么需要缓存

磁盘 I/O 与内存 I/O 的延迟相差几个数量级。当某个接口被高频访问时,如果每次都去查数据库,数据库会承受巨大的压力。此时可以在应用与数据库之间加一层缓存(如 Redis),把热点数据放在内存中:

  • 命中缓存:直接返回,毫秒级响应;
  • 未命中缓存:查询数据库,回写缓存,供后续请求直接读取。

一个典型的"缓存优先"查询流程如下:

  1. 请求到达,先从 Redis 中查目标数据(如用户 ID = 1 的用户信息);
  2. 缓存命中,直接返回,不再访问数据库;
  3. 缓存未命中,走数据库查询;
  4. 查询成功后,把结果写入 Redis(并设置过期时间),再返回给调用方;
  5. 后续相同请求都能直接从缓存拿到数据。

缓存能扛住大部分读流量,但"缓存失效"这个瞬间往往才是灾难的开始。

二、缓存雪崩(Cache Avalanche)

问题是什么

大量缓存数据在同一时间集中失效,导致海量请求瞬间全部打到数据库上,这种现象称为缓存雪崩。

举个例子:假设 Redis 中存储的大量用户信息缓存使用了相同的过期时间,某一刻全部同时过期,紧接着的请求几乎全部穿透到数据库,数据库扛不住,可能导致整个服务不可用。

解决方案

核心思路是打散失效时间,避免"齐刷刷"过期:

  • 设置不同的失效时间:在写入缓存时,为不同的 key 加上随机化的 TTL(如在基础过期时间上增加一个随机偏移),让过期时间分散开;
  • 进一步还可以配合多级缓存、服务降级 / 熔断、缓存预热等手段,降低突发流量对数据库的冲击。

三、缓存穿透(Cache Penetration)

问题是什么

缓存穿透是指查询一个缓存中不存在、同时数据库中也不存在的数据。

比如系统被恶意攻击,循环查询不存在的用户 ID(-1、-2、-3……)。按照正常流程,先查 Redis 没命中,再去查数据库也没有——这类请求每次都穿透缓存打到数据库。如果恶意请求量很大,缓存形同虚设,依然可能压垮数据库。

解决方案

方案一:缓存空值

数据库查不到时,也在 Redis 中缓存一个空值(并设置一个较短的过期时间),后续同类请求直接命中缓存,拦截对数据库的压力。

但要注意:如果被恶意构造大量不同的非法 key,Redis 中会堆积大量无用的空数据,极端情况下甚至会把合法的缓存数据"挤出"(淘汰),导致缓存命中率大幅下降。因此空值必须设置较短 TTL,防止内存被持续占用。

方案二:布隆过滤器(Bloom Filter)

布隆过滤器是一种用于判断"某个元素是否在集合中"的数据结构,特点是空间占用小、查询速度快,代价是存在一定的误判率:

  • 判定"不存在"是绝对准确的;
  • 判定"存在"可能有误判。

处理流程大致如下:

  1. 将数据库中的全部合法数据(如所有用户 ID)加入布隆过滤器;
  2. 查询时若 Redis 未命中,先检查布隆过滤器;
  3. 布隆过滤器认为"不存在"——由于该判定绝对准确,直接响应"数据不存在",不再查库;
  4. 布隆过滤器认为"存在"——走数据库查询,并将结果写入 Redis。即便有少量误判,也只会多缓存少许空值,不会出现大量无用空数据导致合法缓存被驱逐的问题。

四、缓存击穿(Cache Breakdown)

问题是什么

缓存击穿是指某个热点数据在失效的瞬间,大量并发请求同时打到数据库上。

比如某一篇笔记突然爆火,对应的缓存正好过期,一瞬间涌入海量请求,而新缓存还没来得及重建,所有请求都落到数据库,可能导致数据库崩溃。

注意区分:雪崩是"大量 key 同时失效",击穿是"单个热点 key 失效瞬间"被高并发打穿。

解决方案

方案一:缓存不设置过期时间

缓存不过期就不会被删除,自然没有击穿风险。适合基本不会变更的静态热点数据,配合后台定时任务在数据变更时主动更新缓存即可。

方案二:加分布式锁(互斥重建)

当热点 key 失效时,通过分布式锁保证只有一个服务实例去查询数据库并重建缓存,其余请求等待锁释放后直接读取已重建的缓存,避免大量请求同时打库。

方案三:本地缓存 + Redis 形成二级缓存

在应用内引入本地缓存(如 Caffeine),与 Redis 分布式缓存形成两级缓存,且两者的过期时间设置不一致。即使 Redis 缓存过期,本地缓存还能"顶着";随时间流逝,必然只有一个实例的本地缓存先失效,此时只有少量流量落到数据库,查询结果再回填 Redis 并更新该实例的本地缓存。多实例的本地缓存过期时间天然错开,从而把压力分散开。

方案四:热点 Key 探测,动态延长失效时间

在上述方案基础上,还可以引入热点 Key 探测机制:针对访问频率较高、且即将过期的 key,自动为其延长到期时间,从源头避免热点缓存失效。

五、三者对比一览

问题 触发场景 本质 典型解决方案
缓存雪崩 大量 key 同时失效 批量失效 随机/差异化 TTL、多级缓存、降级熔断、缓存预热
缓存穿透 查询缓存与数据库都不存在的数据 无效请求穿透 缓存空值(短 TTL)、布隆过滤器
缓存击穿 单个热点 key 失效瞬间 单点失效被高并发打穿 不设过期时间、分布式锁、二级缓存、热点探测延长 TTL

六、总结

  • 雪崩是"面"的问题:让过期时间随机化,别让缓存"组团过期";
  • 穿透是"空"的问题:要么缓存空值,要么用布隆过滤器挡住不存在的 key;
  • 击穿是"点"的问题:热点 key 用锁、二级缓存或热点探测来"护住"重建窗口。

三者常被混为一谈,面试或设计时先分清"批量失效 / 不存在 / 热点失效",再对号入座选方案,就不会答偏。实际生产中最稳妥的做法通常是组合使用:随机 TTL 防雪崩 + 布隆过滤器/空值缓存防穿透 + 分布式锁与本地二级缓存防击穿,再配合缓存预热与降级熔断兜底。

缓存设计:缓存雪崩、缓存穿透、缓存击穿
https://www.clxhxhhr.top/posts/4588/
作者
clxstart
发布于
2026-10-10
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。