缓存使用不当:穿透、击穿、雪崩的典型坑
说到缓存,大家应该都不陌生。但缓存用不好,坑是真不少。
我个人经验里,最常出问题的就是三个场景——穿透、击穿、雪崩。听起来像自然灾害,其实每个都有特定的触发条件和应对方案。
今天咱们把这几个坑逐一拆开,看看背后到底发生了什么,以及怎么防。
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高可用:主从+哨兵或集群模式,防止单点故障。
核心思路:不要让数据库承受突发流量。把冲击尽量分散到缓存层解决,或者用锁限制并发重建的数量。
三者关系与最佳应对流程
我把这三个问题的关系以及应对措施画了个流程图,方便你整体把握。
我个人实践的建议:别指望一个方案搞定所有问题。混合使用才稳——布隆过滤器防穿透,互斥锁防击穿,TTL随机化+多级缓存防雪崩。这三板斧下去,数据库基本就安全了。
避坑指南:我总结的几个教训
- 别把所有鸡蛋放一个篮子里——Redis挂了怎么办?本地缓存用起来。
- 监控要到位——缓存命中率、Redis延迟、数据库QPS,这三个指标必须上告警。我在项目里就因为没监控Redis的key过期量,雪崩了半小时才发现。
- 测试环境模拟失效——故意把缓存删掉,看数据库能不能撑住。压测不可少。
- 空值缓存要注意内存——如果恶意请求的key空间无限大,空值缓存会撑爆Redis。配合布隆过滤器或者限制key前缀使用。
我曾经犯的一个低级错误:布隆过滤器初始化时用了默认的误判率0.01,结果“非法请求放进去”的概率虽然很低,但恰好被刷子利用,数据库还是被穿透了一小部分。后来把误判率降到0.001,问题解决。代价就是过滤器占用内存大了几倍,但和数据库崩溃相比,这点内存根本不算什么。
好了,三个坑就是这样。你下次加缓存的时候,心里先过一遍——我的代码里有没有穿透、击穿、雪崩的风险? 提前打好补丁,比线上出问题再熬夜排查强得多。