缓存使用不当:穿透、击穿、雪崩的典型坑

说到缓存,大家应该都不陌生。但缓存用不好,坑是真不少。

我个人经验里,最常出问题的就是三个场景——穿透、击穿、雪崩。听起来像自然灾害,其实每个都有特定的触发条件和应对方案。

今天咱们把这几个坑逐一拆开,看看背后到底发生了什么,以及怎么防。

1. 缓存穿透——查了个根本不存在的东西

什么叫穿透?请求的数据在缓存和数据库里都不存在。

比如拿用户ID查用户信息,缓存没命中,去数据库查也没查到。正常的流程是:
先查缓存 → 没命中 → 查数据库 → 数据库也没有 → 返回空。

但问题来了——如果这个空结果不被缓存,那每次请求都会直接穿透到数据库。要是恶意攻击者故意构造一堆不存在的ID,数据库就扛不住了。

典型表现:Redis命中率正常(甚至很高),但数据库QPS突然飙升,CPU打满。

我在项目里遇到过一回。线上用户注册接口被人刷了,对方循环请求不存在的手机号。数据库瞬间被打到连接池满,整个服务响应变慢。排查发现缓存里全是存在的用户,不存在的一律没存。

解决办法

  • 缓存空结果:把数据库回查为空的key也缓存起来,TTL设短一些(比如30秒到2分钟)。这样同样的key不会再打到数据库。
  • 布隆过滤器:把所有可能存在的key(比如已注册的手机号)预加载到布隆过滤器。请求来了先过过滤器,不存在直接返回,连缓存都不用查。

我的习惯:布隆过滤器适合key集合相对固定、数据量大的场景。如果key频繁变更,布隆过滤器的维护成本会很高,不如缓存空结果来得简单。

2. 缓存击穿——热点key突然失效

击穿和穿透不一样。击穿是针对单个热点key。

比如一个热门商品的详情页,缓存里存着它的数据。突然这个key过期了,同一瞬间成千上万的请求涌进来。大家发现缓存没有了,就都去查数据库。
数据库瞬间被这波请求压垮,而缓存里却因为大家都在写,反而可能造成重复加载甚至死锁。

你想想看——一个热点key的失效,能打垮整个数据库。听上去是不是挺吓人?

我曾经踩过的坑:双十一秒杀场景,商品详情页的缓存TTL设了1小时,但是所有商品都在同一时间点过期。零点秒杀开始那个瞬间,数据库QPS直接冲到10万+,挂了。

解决办法

  • 互斥锁(Mutex):查询缓存未命中时,先尝试加锁(比如Redis的SETNX)。拿到锁的线程去查数据库并填充缓存,其他线程等待或重试。避免并发重建缓存。
  • 提前续期:对热点key设置较长的TTL,并在后台异步线程里检测快要到期的key,主动续期。
  • 永不过期 + 异步刷新:缓存不设过期时间,而是用后台任务定时更新。适合对一致性要求不高的场景。

3. 缓存雪崩——大量key同时过期

雪崩是击穿的升级版。不是单个key失效,而是大片key在同一时间过期。

比如缓存里存了全量商品数据,TTL统一设了12小时。凌晨0点一过,所有缓存一起失效。从这个时间点开始,几乎所有请求都落到数据库上。结果就是:数据库瞬间死亡。

更极端的情况是——Redis本身挂了。那更是雪上加霜,所有请求直接打到底层存储。

现象 穿透 击穿 雪崩
范围 单个不存在的key 单个热点key 大量key同时失效
触发条件 查询不存在的数据 热点key过期 + 高并发 同批次缓存同时过期
对数据库影响 持续低QPS穿透 突发高QPS集中冲击 灾难级QPS毛刺
关键防御 缓存空值/布隆过滤器 互斥锁/主动续期 TTL随机化/多级缓存

解决办法

  • TTL随机化:设置缓存过期时间时,在基础时间上增加一个随机偏移(比如 ±5分钟)。避免大量key同时到期。
  • 多级缓存:本地缓存(Caffeine/Guava)扛第一层,Redis扛第二层,数据库扛第三层。哪怕Redis挂了,本地缓存还能撑一会儿。
  • 缓存预热 + 互斥锁:在流量低谷时主动把热点数据加载到缓存。即使出现失效,也用互斥锁控制重建过程。
  • Redis高可用:主从+哨兵或集群模式,防止单点故障。

核心思路:不要让数据库承受突发流量。把冲击尽量分散到缓存层解决,或者用锁限制并发重建的数量。

三者关系与最佳应对流程

我把这三个问题的关系以及应对措施画了个流程图,方便你整体把握。

缓存三大坑·分层应对策略 用户请求 缓存(Redis) 穿透 布隆过滤器 or 缓存空值 击穿 互斥锁 or 主动续期 雪崩 TTL随机化 + 多级缓存 数据库 ⚠️ 三个坑的本质:请求绕过缓存直接冲击数据库 应对核心:分散流量 + 控制并发 + 保护数据库

我个人实践的建议:别指望一个方案搞定所有问题。混合使用才稳——布隆过滤器防穿透,互斥锁防击穿,TTL随机化+多级缓存防雪崩。这三板斧下去,数据库基本就安全了。

避坑指南:我总结的几个教训

  • 别把所有鸡蛋放一个篮子里——Redis挂了怎么办?本地缓存用起来。
  • 监控要到位——缓存命中率、Redis延迟、数据库QPS,这三个指标必须上告警。我在项目里就因为没监控Redis的key过期量,雪崩了半小时才发现。
  • 测试环境模拟失效——故意把缓存删掉,看数据库能不能撑住。压测不可少。
  • 空值缓存要注意内存——如果恶意请求的key空间无限大,空值缓存会撑爆Redis。配合布隆过滤器或者限制key前缀使用。

我曾经犯的一个低级错误:布隆过滤器初始化时用了默认的误判率0.01,结果“非法请求放进去”的概率虽然很低,但恰好被刷子利用,数据库还是被穿透了一小部分。后来把误判率降到0.001,问题解决。代价就是过滤器占用内存大了几倍,但和数据库崩溃相比,这点内存根本不算什么。

好了,三个坑就是这样。你下次加缓存的时候,心里先过一遍——我的代码里有没有穿透、击穿、雪崩的风险? 提前打好补丁,比线上出问题再熬夜排查强得多。