什么是缓存穿透呢?
我们知道,当请求查询缓存未命中时,需要查询数据库以加载缓存。但是大家思考一下这样的场景:
如果我访问一个数据库中也不存在的数据。会出现什么现象?
由于数据库中不存在该数据,那么缓存中肯定也不存在。因此不管请求该数据多少次,缓存永远不可能建立,请求永远会直达数据库。
假如有不怀好意的人,开启很多线程频繁的访问一个数据库中也不存在的数据。由于缓存不可能生效,那么所有的请求都访问数据库,可能就会导致数据库因过高的压力而宕机。
解决这个问题有两种思路:
-
缓存空值
-
布隆过滤器
方案一:缓存空值
简单来说,就是当我们发现请求的数据即不存在与缓存,也不存在与数据库时,将空值缓存到Redis,避免频繁查询数据库。实现思路如下:
优点:
-
实现简单,维护方便
缺点:
-
额外的内存消耗
方案二:布隆过滤器
布隆过滤是一种数据统计的算法,用于检索一个元素是否存在一个集合中。
一般我们判断集合中是否存在元素,都会先把元素保存到类似于树、哈希表等数据结构中,然后利用这些结构查询效率高的特点来快速匹配判断。但是随着元素数量越来越多,这种模式对内存的占用也越来越大,检索的速度也会越来越慢。而布隆过滤的内存占用小,查询效率却很高。
布隆过滤首先需要一个很长的bit数组,默认数组中每一位都是0.
然后还需要K
个hash
函数,将元素基于这些hash函数做运算的结果映射到bit数组的不同位置,并将这些位置置为1,例如现在k=3:
-
hello
经过运算得到3个角标:1、5、12 -
world
经过运算得到3个角标:8、17、21 -
java
经过运算得到3个角标:17、25、28
则需要将每个元素对应角标位置置为1:
此时,我们要判断元素是否存在,只需要再次基于K
个hash
函数做运算, 得到K
个角标,判断每个角标的位置是不是1:
-
只要全是1,就证明元素存在
-
任意位置为0,就证明元素一定不存在
假如某个元素本身并不存在,也没添加到布隆过滤器过。但是由于存在hash碰撞的可能性,这就会出现这个元素计算出的角标已经被其它元素置为1的情况。那么这个元素也会被误判为已经存在。
因此,布隆过滤器的判断存在误差:
-
当布隆过滤器认为元素不存在时,它肯定不存在
-
当布隆过滤器认为元素存在时,它可能存在,也可能不存在
当bit
数组越大、Hash
函数K
越复杂,K
越大时,这个误判的概率也就越低。由于采用bit
数组来标示数据,即便4,294,967,296
个bit
位,也只占512mb
的空间
我们可以把数据库中的数据利用布隆过滤器标记出来,当用户请求缓存未命中时,先基于布隆过滤器判断。如果不存在则直接拒绝请求,存在则去查询数据库。尽管布隆过滤存在误差,但一般都在0.01%左右,可以大大减少数据库压力。
使用布隆过滤后的流程如下: