Skip to content

缓存穿透了怎么办?

前两篇缓存文章主要解决了:

  • 如何使用缓存(Cache Aside)
  • 如何保证缓存高可用(Redis Cluster)

但缓存系统上线后,很快会遇到一个现实问题:

有些请求根本无法被缓存拦住。

正常情况下:

text
请求

Redis

命中

返回

数据库压力很小。

但某些请求会变成:

text
请求

Redis不存在

MySQL不存在

返回空

而且每次请求都会重复这个过程。

这种现象就是:

缓存穿透(Cache Penetration)

本章核心讨论的就是:

如何防止大量无效请求绕过缓存,直接冲击数据库。

一、什么是缓存穿透

缓存穿透指的是:

查询一个缓存中不存在、数据库中也不存在的数据。

例如:

text
用户ID = -1
订单ID = 999999999999

这些数据本来就不存在。

查询过程:

text
Redis
没有



MySQL
没有



返回空

如果:

正常用户偶尔查一次。

问题不大。

但如果:

text
10000 QPS

都在查询不存在的数据。

那么:

text
10000次请求

全部打到数据库

缓存完全失效。

二、缓存穿透为什么危险

缓存设计的目标是:

保护数据库。

正常情况:

text
10000请求

Redis

9900命中

100到数据库

数据库压力很小。

但穿透发生时:

text
10000请求

Redis

全部未命中

MySQL

数据库瞬间承受:

text
10000 QPS

如果是恶意攻击:

甚至:

text
10万QPS
100万QPS

数据库可能直接被打垮。

因此:

缓存穿透本质上是:

无效请求绕过缓存,对数据库发动攻击。

三、缓存穿透产生的根本原因

文章强调:

缓存穿透的根源是:

“不存在的数据没有被缓存”。

例如:

数据库:

text
不存在 user:999999

Redis:

text
也不存在 user:999999

于是:

每次查询:

text
Redis Miss

DB Miss

返回

下次查询:

仍然:

text
Redis Miss

DB Miss

无限重复。

四、方案一:缓存空对象(最常用)

这是最简单也是最常见的方案。

第一次查询:

text
user:999999

结果:

text
Redis
没有



MySQL
没有

此时:

缓存一个特殊值:

text
NULL

例如:

text
user:999999 = null

并设置:

text
TTL = 5分钟

下次请求:

text
Redis
命中NULL

直接返回。

数据库不会再被访问。

五、缓存空对象的优缺点

优点:

实现简单

几乎所有系统都能快速落地。

效果明显

绝大部分穿透问题立即解决。

缺点:

占用缓存空间

例如:

攻击者不断请求:

text
user:100000001
user:100000002
user:100000003

Redis会存储大量:

text
NULL Key

导致:

缓存空间浪费。

因此:

一般会设置:

text
较短TTL

例如:

text
5分钟
10分钟

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

这是文章重点介绍的方案。

核心思想:

在访问 Redis 之前,先判断数据是否可能存在。

流程变成:

text
请求

Bloom Filter


不存在

直接返回

存在

Redis

MySQL

例如:

系统有:

text
1亿用户

所有合法用户ID:

提前加入 Bloom Filter。

查询:

text
user:999999999

时:

Bloom Filter判断:

text
绝对不存在

直接拒绝。

无需访问:

text
Redis
MySQL

七、布隆过滤器为什么高效

因为:

Bloom Filter本质是:

位数组 + 多个Hash函数。

特点:

空间极小

例如:

text
1亿数据

可能只需要:

text
几百MB

远小于:

text
HashMap

查询极快

时间复杂度:

text
O(1)

适合:

高并发场景。

八、布隆过滤器的特点

文章特别强调:

Bloom Filter有一个重要特点:

不存在误判不存在

即:

如果判断:

text
不存在

那么一定不存在。

但:

可能误判存在

例如:

实际上不存在:

text
user:999999

Bloom Filter可能认为:

text
存在

于是:

继续访问:

text
Redis
MySQL

但不会影响正确性。

只是降低一点效果。

九、缓存空对象 vs 布隆过滤器

缓存空对象

优点:

  • 实现简单
  • 无需额外组件

缺点:

  • 浪费缓存空间
  • 需要等待第一次请求

Bloom Filter

优点:

  • 请求还没到 Redis 就被拦截
  • 节省缓存空间
  • 更适合大规模系统

缺点:

  • 实现复杂
  • 存在误判率

十、互联网公司的实际做法

文章给出的实践方案:

中小系统

通常:

text
缓存空对象

已经足够。

大型系统

通常:

text
Bloom Filter
+
缓存空对象

组合使用。

流程:

text
请求

Bloom Filter

Redis

MySQL

形成多层保护。

十一、缓存穿透与缓存击穿的区别

很多人容易混淆。

缓存穿透

查询:

text
不存在的数据

特点:

text
Redis没有
DB也没有

缓存击穿

查询:

text
热点数据

特点:

text
Redis过期
DB有

因此:

穿透攻击的是:

text
不存在的数据

击穿攻击的是:

text
热点数据

两者完全不同。

十二、本章最重要的工程思想

缓存并不能天然挡住所有流量。

因为:

对于不存在的数据,缓存根本没有内容可缓存。

因此:

必须增加额外防线。

整个防御体系:

text
请求

Bloom Filter

Redis

MySQL

核心目标是:

让无效请求尽量在最前面被拦截。

不要进入数据库。

十三、本章核心脉络

text
引入缓存

发现数据库仍被打爆

分析发现大量不存在的数据查询

缓存穿透


解决方案:

缓存空对象

Bloom Filter


组合防御

保护数据库

一句话总结

缓存穿透是指查询的数据既不在缓存中也不在数据库中,导致所有请求直接落到数据库。解决方案主要有缓存空对象和布隆过滤器两种:前者通过缓存空结果避免重复查询,后者通过预先判断数据是否存在来提前拦截非法请求。在大型系统中,通常将两者结合使用,在数据库之前构建多层防护体系。

Released under the MIT License.