39.如何避免缓存”穿透”的问题?
缓存穿透
缓存穿透,是指查询一个一定不存在的数据,由于缓存是不命中时被动写,并且处于容错考虑,如果从 DB 查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,失去了缓存的意义。
被动写:当从缓存中查不到数据时,然后从数据库查询到该数据,写入该数据到缓存中。
在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。如下图:
缓存穿透
- 在 [「为什么要用缓存?」 中,我们已经看到,MySQL 的性能是远不如 Redis 的,如果大量的请求直接打到 MySQL ,则会直接打挂 MySQL 。
- 当然,缓存穿透不一定是攻击,也可能是我们自己程序写的问题,疯狂读取不存在的数据,又或者“无脑”的爬虫,顺序爬取数据。
- 另外,一定要注意,缓存穿透,指的是查询一个不存在的数据,很容器和我们要讲到的缓存击穿搞混淆。
? 如何解决
有两种方案可以解决:
1)方案一,缓存空对象。
当从 DB 查询数据为空,我们仍然将这个空结果进行缓存,具体的值需要使用特殊的标识,能和真正缓存的数据区分开。另外,需要设置较短的过期时间,一般建议不要超过 5 分钟。
为什么要较短的过期时间?因为缓存久没有意义,也浪费缓存的内存。
2)方案二,BloomFilter 布隆过滤器。
在缓存服务的基础上,构建 BloomFilter 数据结构,在 BloomFilter 中存储对应的 KEY 是否存在,如果存在,说明该 KEY 对应的值不为空。那么整个逻辑的如下:
- 1、根据 KEY 查询【BloomFilter 缓存】。如果不存在对应的值,直接返回;如果存在,继续向下执行。【后续的流程,就是标准的流程】
- 2、根据 KEY 查询在【数据缓存】的值。如果存在值,直接返回;如果不存在值,继续向下执行。
- 3、查询 DB 对应的值,如果存在,则更新到缓存,并返回该值。
可能有胖友不是很了解 BloomFilter 布隆过滤器,会有疑惑,为什么 BloomFilter 不存储 KEY 是不存在的情况(就是我们方案二反过来)?
- BloomFilter 存在误判。简单来说,存在的不一定存在,不存在的一定不存在。这样就会导致,一个存在的 KEY 被误判成不存在。
- 同时,BloomFilter 不允许删除。例如说,一个 KEY 一开始是不存在的,后来数据新增了,但是 BloomFilter 不允许删除的特点,就会导致一直会被判断成不存在。
当然,使用 BloomFilter 布隆过滤器的话,需要提前将已存在的 KEY ,初始化存储到【BloomFilter 缓存】中。
? 选择
这两个方案,各有其优缺点。
缓存空对象 | BloomFilter 布隆过滤器 | |
---|---|---|
适用场景 | 1、数据命中不高 2、保证一致性 | 1、数据命中不高 2、数据相对固定、实时性低 |
维护成本 | 1、代码维护简单 2、需要过多的缓存空间 3、数据不一致 | 1、代码维护复杂 2、缓存空间占用小 |
实际情况下,使用方案二比较多。因为,相比方案一来说,更加节省内容,对缓存的负荷更小。
注意,常用的缓存 Redis 默认不支持 BloomFilter 数据结构。具体怎么解决,参考如下文章:
-
Redis 4.0 引入 Module 机制,支持 Server 自定义拓展。而 RedisBloom ,就是 Redis BloomFilter 的拓展。
Redis-Lua-scaling-bloom-filter
Lua 脚本,实现 BloomFilter 的功能。
-
Java Redis 库,实现 BloomFilter 的功能。
其它文章
因为 BloomFilter 布隆过滤器存在的误判的情况,如果最后去 DB 查询不到数据的情况,是不是可以结合方案一,缓存空对象到【BloomFilter 缓存】中。后来想想,必要性不大,因为 BloomFilter 布隆过滤器误判率很低,没必要把方案复杂化,大道至简。
另外,推荐看下 《Redis架构之防雪崩设计:网站不宕机背后的兵法》 文章的 「一、缓存穿透预防及优化」 ,大神解释的更好,且提供相应的图和伪代码。