故事场景:小区门口的小卖部
把系统想成一家小区小卖部:
- 小卖部货架 = Redis 缓存:东西摆在眼前,拿起来就走,快;
- 城外大仓库 = 数据库:东西全,但跑一趟远,慢;
- 顾客 = 一次请求。
正常流程是:顾客先问小卖部有没有,有就直接拿走;没有,小卖部才跑一趟仓库进货,顺手在货架上补一件,下次再来就不用跑了。
缓存出问题,基本都是这个流程在某个环节被冲垮了。下面三种,名字像,病因完全不同,别混着背。
第一病:缓存穿透——天天来问一件根本不存在的货
有个顾客天天来问:“有恐龙蛋吗?”小卖部没有,跑仓库问,仓库说也没有。问题是:这次白跑的结果没记下来,第二天他又来问,小卖部又白跑一趟。问的人一多,或者干脆是恶意刷的,仓库就被这种“查无此物”的请求拖垮了。
特征很好认:缓存和数据库都没有这个数据,每次请求都直达数据库。
解法有三个,经常一起用:
- 参数先验货:明显非法的请求(比如 id 是负数、格式不对)直接挡在门外,别让它进仓库;
- 把“没有”也记下来:查不到时,往缓存里写一个空值,只存很短时间(比如几十秒)。下次再问,先看到“刚问过,没有”,就不用再跑仓库了。空值 TTL 一定要短,不然正常上架的新货会被这个“没有”挡住一阵子;
- 门口挂一块布隆过滤器:相当于一份“本店可能有货的清单”。清单说没有,就一定没有,直接拒绝;清单说可能有,才放行去货架上找。注意布隆过滤器只会“误报有货”,不会“漏报无货”,所以它挡得住穿透,挡不住正常请求——这正是我们要的。
第二病:缓存击穿——爆款刚下架,全小区同时涌进来
小卖部有一款爆款饮料,货架上只剩最后一瓶,偏偏这时候它过期了(缓存 TTL 到期)。同一秒,全小区的人都来买这一款:货架上没有,每个人都让小卖部跑仓库,仓库门口瞬间排起长队,正常生意全被堵死。
特征也很好认:就一个热点 key 过期,但并发全压在它身上。
解法核心就一句话:别让所有人都去跑仓库,只放一个人去,其他人等着。
- 互斥锁:第一个发现没货的人先去仓库,其他人先等一会儿再回来看货架——等锁的人看到货补上了,直接拿走,不用再跑;
- 热点 key 永不过期 + 后台悄悄补货:爆款干脆不设过期时间,后台定时去仓库看看有没有新货、有就悄悄补上。前台永远有货卖,过期那一秒的“真空期”根本不存在。代价是后台要多跑几趟,适合真正经得起这个成本的热点。
面试时常考的区分点就在这里:击穿是“一个 key 被打穿”,穿透是“根本不存在的 key 被反复问”。
第三病:缓存雪崩——不是一款断货,是整排货架同时过期
更惨的是第三种:小卖部为了图省事,给所有货设了同一个过期时间。到了整点,整排货架同时空了,全小区的人涌进来,小卖部只能一趟趟跑仓库;或者更直接——小卖部本身停电了(Redis 宕机),所有请求一股脑全压到仓库,仓库直接被压垮。
特征:大量 key 同时失效,或者缓存服务本身不可用,数据库瞬间被打满。
解法要分层,不能只靠一招:
- 过期时间打散:给每个 key 的 TTL 加一个随机数,别让它们同一秒集体到期——这是成本最低、最先该做的一招;
- 多级缓存:本地缓存顶一层、Redis 顶一层,Redis 倒了,本地缓存还能撑一阵子,给恢复争取时间;
- 限流 + 熔断:仓库门口设个闸,超过承受能力的请求先排队或直接返回“稍后再试”,宁可慢一点、拒绝一部分,也别让数据库被拖死后再全军覆没;
- 缓存高可用:Redis 本身做主从或集群,别让单点故障变成全站故障。
一张表记住三者的区别
| 问题 | 现象 | 本质 | 第一招 |
|---|---|---|---|
| 穿透 | 查不存在的数据 | 缓存、数据库双双落空 | 空值缓存 / 布隆过滤器 |
| 击穿 | 一个热点 key 过期 | 并发全压在同一个 key 上 | 互斥锁 / 热点永不过期 |
| 雪崩 | 大量 key 同时失效或缓存宕机 | 数据库被瞬间压垮 | TTL 打散 / 多级缓存 / 限流熔断 |
简单粗暴地说: 穿透防“无中生有”,击穿防“单点被围”,雪崩防“集体垮塌”。面试官问起来,先报现象、再报本质、最后报解法,这题就是送分题。
和这篇搭着看:缓存解决的是“读得快”,读的数据从哪来、怎么存得干净,见《关系型数据库设计三大范式到底是什么?》;数据量大了查得慢、索引怎么选,见《MySQL 索引为什么用 B+ 树?新华字典目录里藏着的秘密》。
动手验证:上面这些招,用一台小云服务器装个 Redis 就能亲手复现——先压穿透、再压击穿,看着数据库负载曲线变化,比背十遍都管用。选购可以看我整理的云服务器选购指南,新客首单很便宜,别买贵了。