前几天我们讲到了缓存的读写策略(你一定要掌握这种缓存读写策略,开发必备)以及如何搭建高可用缓存系统(分布式缓存高可用方案,我们都是这么干的),都是为了能在基础架构上让我们的缓存命中率能更高,防止大量的请求直接穿透我们的后端存储系统例如MySQL数据库,造成数据库的带宽和连接骤升,从而拖垮我们的整个业务。
按照互联网常理来说,我们的核心业务缓存模块命中率要达到99%,非核心业务命中率也要达到90%,当然如果是不关注缓存命中率的业务就令当再论了。
既然缓存的穿透会给我们系统带来这么大的麻烦,那我们该怎么处理并且去预防这种穿透带来的灾难呢?今天我们就来讲讲该怎么去防止缓存穿透。
01
什么是缓存穿透
缓存穿透最直白的意思就是,我们的业务系统在接收到请求时在缓存中并没有查到数据,从而穿透到了后端数据库里面查数据的过程。
当然,既然使用了缓存,肯定会难免有穿透的发生,正常的少量穿透是对我们业务来说是不会造成任何影响的,因为:
-
毕竟我们的缓存容量有限,不可能去缓存所有数据,当面临较大请求时,查询到未被缓存的数据时,就会发生穿透。
-
互联网业务的数据访问模型一般是遵循“二八”原则的,即 20% 的数据为热点数据,80% 的数据是非热点不被常访问的数据。
现在既然我们的缓存容量有限,然后 20% 的数据为热点数据,也就是说,我们可以利用有限的容量去缓存那 20% 的数据,其实就是可以保护我们的后端系统的,至于80%非热点不常用的数据发生穿透了,是我们能够接受的。
那究竟什么的缓存穿透会影响到我们的系统呢?是大量的穿透请求超过了我们后端系统的承受范围,比如恶意的穿透攻击,这样的穿透就很有可能把我们的系统给干崩溃。接下来,我们就来基于相关应用场景来解决这种缓存穿透。
02
缓存穿透如何解决
场景:
在我们APP的在线搜索相关系统里面,有个产品product 1 并没有在数据库中进行存储,现在通过cache aside pattern 策略()查这个product 1 。
那查询一个数据库中本身就没有的数据后面会怎样呢?依照cache aside 策略,读取时,首先会读取缓存,没读到数据就会穿透到读数据库,现在数据库也没有,也就没有数据写回缓存。那么,再来个请求依然如此,更多的请求来还是一样,这样的缓存就没意义了。
通过上面场景我们可以看到,这样的系统面临非正常的穿透是会崩溃掉的,那我们该怎么去解决呢?一般我们对此有两种方案,都是有用的:
-
设置空值
-
布隆过滤器
1设置空值
通过上面场景我们知道,当有大量恶意的穿透请求到数据库,就会给我们系统带来灾难。
所以,当我们请求数据中没有数据或者因为代码bug带来的异常造成的数据为空,这个时候我们就可以回写一个空值null到缓存中。同时,我们还要给这个null值设置过期时间,因为这个空值不具有实际业务性,而且还占用空间。
可见设置空值是可以阻挡大量穿透请求的,但是如果有大量的获取并不存在数据的穿透请求的话例如恶意攻击,则会浪费缓存空间,如果这种null值过量的话,还会淘汰掉本身缓存存在的数据,这就会使我们的缓存命中率下降。
生产建议,在使用设置空值方案时,我们要做好监控,预防缓存空间被过多null值占领造成的缓存空间浪费,如果这种数据量太大,就不再建议使用,那就使用另一种方案,即布隆过滤器。
2布隆过滤器
布隆过滤器核心思想,我们把一个集合的每一个元素按照某种 hash 算法计算 hash 值,然后将hash 值对定义的数据长度取模,得到的值即为存在数组的索引。并将该索引位置值从 0 改为 1 。然后我们判断一个元素是否在这个集合的时候,只需要对这个元素计算出数组的索引值,如果这个索引位置的值为 1 则证明该元素在集合内,反之则知道不在这个集合中。
下面我们通过画个图来具体的看看布隆过滤器的工作模式,帮助大家更好的理解和应用
如上图所示,A, B, C三个元素在一个集合里面,拿到一个 D 元素,然后计算它的hash值对应于数组的位置值为 1 则表示这个 D 元素也在ABC的集合里面;接着拿到元素E 同样的计算,发现对应于数组中值为 0 则表示元素 E不在集合中。
03
布隆过滤器如何解决缓存穿透?
通过上面的讲解,相信大家都知道了布隆过滤器的作用了,肯定也知道怎么去用了,那回到我们今天的主角身上,下面我们就使用布隆过滤器来解决我们缓存的穿透问题。
-
首先我们初始化一个数组,比如长度为 20 亿。
-
选用一个 hash 算法,将现有产品比如上面场景的product_id 进行hash计算然后进行映射数组中。
-
映射到的数组值设为 1 ,其他的均为 0 。
-
对于新增的产品在写数据库之外,还要依照同样hash算法映射数组,更新对应位置的值。
-
当查询一个产品的时候,先查询这个产品是否在布隆过滤器里面,如果不在,则直接返回空给客户端,不直接穿透到数据库和缓存中。
-
这样就杜绝了恶意查询请求所带来的缓存穿透。
布隆过滤器性能如何?
你可能会感到疑惑,所有请求先去判断布隆过滤器,这个性能到底怎么样?
它是基于二进制数组的,数组的查询效率应该不用我多说吧,所以不管是读取还是写入,布隆过滤器的时间复杂度是O(1),即常量值。
在空间存储上,同样具有很大的优势,例如,我们20 亿的数组需要 2000000000/8/1024/1024 = 238M 的空间,而如果使用数组来存储,假设每个用户 ID 占用 4 个字节的空间,那么存储 20 亿用户需要 2000000000 * 4 / 1024 / 1024 = 7600M 的空间,是布隆过滤器的 32 倍。
布隆过滤器有什么缺陷?
纵然布隆过滤器表现的很优异,但是任何事务都不会是完美的,它也是有相应缺陷的。
-
由于布隆过滤器是由二进制数组和hash算法组成的,有hash算法就有hash碰撞的发生,像我们的hashmap那样(为什么你每次被问到HashMap底层原理都一知半解,搞定它),所以就有可能将并不在集合中的元素判断在里面。
-
布隆过滤器不支持删除元素
如何解决解决布隆过滤器的缺陷?
对于第一种缺陷,我们可以采用多个hash 算法对其计算,然后比对,多个hash映射的结果都为 1 的话,我们就判定这个元素在集合中。
对于第二个缺陷,如果不能接受的话,我们改变改变下策略,当有相同hash值时,我们就存计数值,例如,A B 相同,存的值为 2 ,不在存 bit位了,这样就会带来存储空间的消耗。所以,我们在使用时需要根据自身业务考虑,如果是不怎么删除的场景下,还是可以考虑使用布隆过滤器来解决缓存穿透问题的。
生产建议:
-
采用多个hash 算法计算hash 值,这样可以减少误判的几率。
-
布隆过滤器会消耗一定内存空间,根据业务场景进行评估需要多大内存,最后依据公司资源以及成本,看是否能够接受。
综上所述,回种空值和布隆过滤器是解决缓存穿透问题的两种最主要的解决方案,但是它们也有各自的适用场景,并不能解决所有问题。
总结,今天我们通过大量请求穿透到数据库中,学习了两种主要的缓存穿透方案:设置空值和布隆过滤器,所以我们解决缓存穿透问题的核心目标在于减少对于数据库的并发请求。