Redis 缓存击穿、穿透、雪崩,到底应该怎么区分和处理?先看一次不存在商品被爬虫打穿的事故
真到线上时,最先有价值的不是把穿透、击穿、雪崩背成定义,而是先证明请求打的到底是不是一批本来就不存在的数据。这次事故里,数据库被压高不是因为缓存整体失效,而是爬虫持续请求无效商品 ID,空结果又没有被挡住。
很多缓存事故一出现场,群里第一句就是:“是不是雪崩了?”
这句话不能说完全没用,但多数时候它太大了。真到排障,先把事故叫成雪崩、击穿还是穿透,没有先看证据重要。我印象最深的一次,是商品详情接口凌晨被打高,数据库读 QPS 抬了三倍,应用线程池也开始排队。最开始大家都以为是热商品缓存一起过期,后来发现完全不是那回事。
真正的首证据,是数据库里大量查不到结果的 skuId。
先把这次事故说清楚
那天 02:17 起,/product/detail 的请求量突然抬高,但流量长得不正常:
- 单 IP、单 UA 的请求占比很高
skuId呈连续递增扫描- Redis miss 之后,数据库也查不到对应商品
把 5 分钟样本拉出来看,新增回源里有 78% 的 skuId 在数据库里根本不存在,而且这些 ID 会被重复请求。也就是说,问题的起点不是“存在的数据失效了”,而是“本来就不存在的数据,正在被持续往下打”。
这时如果还把它当成雪崩去处理,方向就已经偏了。
为什么这次更像穿透,而不是击穿或雪崩
因为三个关键事实都对得上。
第一,查的是不存在的数据。数据库查不到,这是最硬的一条。
第二,请求并不集中在少数热点 key 上,所以不像击穿。热点击穿通常会看到一小撮 key 突然特别热;这次不是,它是很多无效 skuId 被批量扫。
第三,也没有看到统一过期或大面积 key 一起失效的时间特征,所以不像雪崩。Redis 集群本身也没抖,expired_keys 没尖刺,节点 RT 也基本正常。
把这三层一拆,类型其实很清楚:这是一次典型的缓存穿透,只是现场表现出来的结果层跟别的缓存事故很像而已。
事故为什么会被放大
因为系统对“查不到”这件事太宽容了。
当时的详情接口流程是:
- 先查 Redis
- miss 后查数据库
- 数据库查不到就返回空
- 不缓存空结果
这个设计在正常业务量下不一定出事,可一旦有人反复打无效 ID,它等于在说:欢迎你每次都来数据库确认一遍。
更糟的是,入口参数校验也很弱。只要 skuId 是个整数就会往后走,既没有布隆过滤器,也没有空值缓存,更没有针对异常 ID 模式的限流。
所以事故不是一下子“炸”的,而是被系统一层层放大出来的。
真正把方向定下来的,是“查不到占比”这条线
那次排障中最值钱的数字不是命中率,也不是总 QPS,而是:
- 数据库回源查询里,
not found占比从平时 3% 跳到 64% - 同一批无效
skuId在 10 分钟内会被重复查询 5 到 20 次
只要这两个数字立住,后面的动作基本就确定了。
你不需要先讨论“是不是要多级缓存”“要不要逻辑过期”“要不要互斥重建”。这些都不是这次事故的第一手工具。第一手工具反而更简单:先把无效请求挡住,别让数据库继续替爬虫验货。
这次止血动作为什么有效
当时做了三步,而且顺序不能乱:
- 在网关上按 UA 和请求速率先限一层异常流量
- 对数据库查不到的
skuId回写 60 秒空值缓存 - 补一层商品 ID 布隆过滤器,先挡掉明显不存在的请求
动作下去以后,回落顺序很清楚:
- 网关异常流量先降
- 数据库
not found查询量在 2 分钟内掉下来 - 应用线程池 active 和详情页 p99 才慢慢恢复
这个顺序很能说明问题:真正该先止血的,不是 Redis 本身,也不是数据库调优,而是无效请求这条穿透链。
为什么我不想把这篇再写成三类定义手册
因为真实事故里,分型不是拿来背的,是拿来裁剪排查方向的。
像这次,真正决定方向的不是“穿透的定义是什么”,而是一个很具体的判断:数据库是不是在为一批本来就不存在的数据反复买单。 这句话一旦成立,就别再花时间讨论热点击穿和 TTL 集中过期了。
当然,击穿和雪崩在线上也会发生,但它们的首证据跟这次完全不同:
- 击穿更像少数热点对象过期后被并发一起打穿
- 雪崩更像一批 key 在同一时间段成片失效,或者缓存层整体异常
这几类问题最后都可能把数据库打高、把接口拖慢,可起点不一样,第一步动作也不一样。
什么时候该先往“穿透”上收
如果你现场有下面这些特征,就很值得先按穿透处理:
- Redis miss 后,数据库大量返回空
- 无效 ID 或非法参数请求占比异常升高
- 请求分布不是集中在少数热点 key,而是扫一大片无效数据
- 入口校验、空值缓存和布隆过滤器都偏弱
这种形状下,再去优先讨论 TTL、热 key、本地缓存,通常都不是最短路径。
回到这次事故真正留下来的经验
缓存击穿、穿透、雪崩当然需要区分,但线上最值钱的不是把三个词解释得多完整,而是先抓住最硬的一条证据。
这次最硬的证据,就是数据库里那一大片查不到的 skuId。只要这一步看清了,整件事就从“缓存是不是雪崩了”变成了一个更具体、也更能处理的问题:谁在持续请求不存在的数据,而我们为什么每次都让它打到数据库。