缓存穿透了怎么办?
前两篇缓存文章主要解决了:
- 如何使用缓存(Cache Aside)
- 如何保证缓存高可用(Redis Cluster)
但缓存系统上线后,很快会遇到一个现实问题:
有些请求根本无法被缓存拦住。
正常情况下:
请求
↓
Redis
↓
命中
↓
返回数据库压力很小。
但某些请求会变成:
请求
↓
Redis不存在
↓
MySQL不存在
↓
返回空而且每次请求都会重复这个过程。
这种现象就是:
缓存穿透(Cache Penetration)
本章核心讨论的就是:
如何防止大量无效请求绕过缓存,直接冲击数据库。
一、什么是缓存穿透
缓存穿透指的是:
查询一个缓存中不存在、数据库中也不存在的数据。
例如:
用户ID = -1
订单ID = 999999999999这些数据本来就不存在。
查询过程:
Redis
没有
↓
MySQL
没有
↓
返回空如果:
正常用户偶尔查一次。
问题不大。
但如果:
10000 QPS都在查询不存在的数据。
那么:
10000次请求
↓
全部打到数据库缓存完全失效。
二、缓存穿透为什么危险
缓存设计的目标是:
保护数据库。
正常情况:
10000请求
↓
Redis
↓
9900命中
↓
100到数据库数据库压力很小。
但穿透发生时:
10000请求
↓
Redis
↓
全部未命中
↓
MySQL数据库瞬间承受:
10000 QPS如果是恶意攻击:
甚至:
10万QPS
100万QPS数据库可能直接被打垮。
因此:
缓存穿透本质上是:
无效请求绕过缓存,对数据库发动攻击。
三、缓存穿透产生的根本原因
文章强调:
缓存穿透的根源是:
“不存在的数据没有被缓存”。
例如:
数据库:
不存在 user:999999Redis:
也不存在 user:999999于是:
每次查询:
Redis Miss
↓
DB Miss
↓
返回下次查询:
仍然:
Redis Miss
↓
DB Miss无限重复。
四、方案一:缓存空对象(最常用)
这是最简单也是最常见的方案。
第一次查询:
user:999999结果:
Redis
没有
↓
MySQL
没有此时:
缓存一个特殊值:
NULL例如:
user:999999 = null并设置:
TTL = 5分钟下次请求:
Redis
命中NULL直接返回。
数据库不会再被访问。
五、缓存空对象的优缺点
优点:
实现简单
几乎所有系统都能快速落地。
效果明显
绝大部分穿透问题立即解决。
缺点:
占用缓存空间
例如:
攻击者不断请求:
user:100000001
user:100000002
user:100000003Redis会存储大量:
NULL Key导致:
缓存空间浪费。
因此:
一般会设置:
较短TTL例如:
5分钟
10分钟六、方案二:布隆过滤器(Bloom Filter)
这是文章重点介绍的方案。
核心思想:
在访问 Redis 之前,先判断数据是否可能存在。
流程变成:
请求
↓
Bloom Filter
↓
不存在
↓
直接返回
存在
↓
Redis
↓
MySQL例如:
系统有:
1亿用户所有合法用户ID:
提前加入 Bloom Filter。
查询:
user:999999999时:
Bloom Filter判断:
绝对不存在直接拒绝。
无需访问:
Redis
MySQL七、布隆过滤器为什么高效
因为:
Bloom Filter本质是:
位数组 + 多个Hash函数。
特点:
空间极小
例如:
1亿数据可能只需要:
几百MB远小于:
HashMap查询极快
时间复杂度:
O(1)适合:
高并发场景。
八、布隆过滤器的特点
文章特别强调:
Bloom Filter有一个重要特点:
不存在误判不存在
即:
如果判断:
不存在那么一定不存在。
但:
可能误判存在
例如:
实际上不存在:
user:999999Bloom Filter可能认为:
存在于是:
继续访问:
Redis
MySQL但不会影响正确性。
只是降低一点效果。
九、缓存空对象 vs 布隆过滤器
缓存空对象
优点:
- 实现简单
- 无需额外组件
缺点:
- 浪费缓存空间
- 需要等待第一次请求
Bloom Filter
优点:
- 请求还没到 Redis 就被拦截
- 节省缓存空间
- 更适合大规模系统
缺点:
- 实现复杂
- 存在误判率
十、互联网公司的实际做法
文章给出的实践方案:
中小系统
通常:
缓存空对象已经足够。
大型系统
通常:
Bloom Filter
+
缓存空对象组合使用。
流程:
请求
↓
Bloom Filter
↓
Redis
↓
MySQL形成多层保护。
十一、缓存穿透与缓存击穿的区别
很多人容易混淆。
缓存穿透
查询:
不存在的数据特点:
Redis没有
DB也没有缓存击穿
查询:
热点数据特点:
Redis过期
DB有因此:
穿透攻击的是:
不存在的数据击穿攻击的是:
热点数据两者完全不同。
十二、本章最重要的工程思想
缓存并不能天然挡住所有流量。
因为:
对于不存在的数据,缓存根本没有内容可缓存。
因此:
必须增加额外防线。
整个防御体系:
请求
↓
Bloom Filter
↓
Redis
↓
MySQL核心目标是:
让无效请求尽量在最前面被拦截。
不要进入数据库。
十三、本章核心脉络
引入缓存
↓
发现数据库仍被打爆
↓
分析发现大量不存在的数据查询
↓
缓存穿透
↓
解决方案:
缓存空对象
↓
Bloom Filter
↓
组合防御
↓
保护数据库一句话总结
缓存穿透是指查询的数据既不在缓存中也不在数据库中,导致所有请求直接落到数据库。解决方案主要有缓存空对象和布隆过滤器两种:前者通过缓存空结果避免重复查询,后者通过预先判断数据是否存在来提前拦截非法请求。在大型系统中,通常将两者结合使用,在数据库之前构建多层防护体系。