在常见的分布式架构中,Redis为缓存的主要组件。在这种系统架构中,Redis多做热点数据存储,为MySQL提供缓冲。在实际场景中,可能会有如下三个问题。
一、缓存穿透
当大量的假数据访问时,这种数据实际上 MySQL 中是不存在的,Redis中更没有,但由于分布式架构中,Redis作为MySQL的缓冲,会先访问Redis,显然没有,再去访问MySQL,这就造成这样的请求,每次都会访问Redis和MySQL。如果大量这样请求,就会我们服务挂掉,也算是攻击漏洞。
解决策略:
如果我们事先知道我们并不存在这样的数据,也就避免再去访问Redis和MySQL。那么问题就是,如果我们有十万或者上亿的数据,该如何存储这些数据并快速查询某一数据是否存在的问题了。
假如我们存储每条数据本身的内容,假如一条数据有几字节,那么总的数据体量就可能是上百G。所以不能存储数据本身。那么如果我们只需要存储每条数据的状态,即是否存在,那么就可以解决存储的问题了。如借用链表中“桶”的概念,如果有这样一个数组,数组的每个单元值只有“0”和“1”,分别代表“不存在”和“存在”两种状态,然后对数据的key进行hash可以快速定位到数组的某一索引。这样当我们去查询某一个key的数据是否存在时,则只需要先对key hash得到数组中的索引位置,然后看位置处的数值即可。这样存储下来,也只需要几兆的空间。
在大部分设计语言中,都存在有一种“位数组”的数据结构,如Java的BitSet。连续的数组,每个数组单元只有“0”和“1”。我们先对key hash,然后就可以定位出一个索引,然后将其数值赋为“1”,即数据存在。对所有的数据依次这样处理。但我们都知道hash算法是关键,如果不合适的算法,可能造成连续的数组空间只利用了部分单元,同时分布还不均匀,造成整体数组空间较大,同时,hash冲突的概率也很高。所以一般都是rehash,几次hash处理的。当查询数据时,也是利用对key进行存储时所用相同hash算法计算来确定数组索引。这样过程,就是所谓的“布隆过滤器”设计!
我们都知道hash冲突是不可能避免的,就算hash算法再怎么好,也只是把碰撞的几率降低而已。所以,如果一个key 多次hash后,定位的数组单元上数值为“1”也不代表该key的数据就一定存在,因为该数组单元可能也是另外一个key hash后的位置。但反之,如果数值为“0”,则一定说明key对应的数据不存在。因为存储和查找都是用的同一hash算法,并且hash算法,必然遵循对同一值每次hash都是同样的结果。所以查询时,为“0”,就说明,存储时就没有对应的key hash后结果为该索引。
所以“布隆过滤器”的特点:
1、如果查询数据,结果为不存在,则数据一定不存在;
2、如果查询数据,结果存在,不一定代表数据就真的存在;
利用“布隆过滤器”这种方式,就可以避免缓存击穿的问题。只需要事先在服务初始化时,完成数据向“布隆过滤器”的填充。然后在每次请求到来,访问redis前,先访问“布隆过滤器”,如果不存在则直接返回,否则再去向下查询。这样就降低了下层数据层服务因不存在数据连续访问攻击而挂掉的可能。“布隆过滤器”在内存中存储,且不占用多少空间,所以速度也非常快。
具体实践,可以利用Google的Guava包中的 BloomFilter ,提供了create函数,可以指定错误率(也就是一定的碰撞率)和数据容量,它会根据错误率和数据容量来计算出底层“位数组”的大小。错误率指定范围在“0~1”之间,不包含边界0和1,不能为0,就是因为hash冲突不可避免。当然如果错误率要求低的话,查询性能也会低一些,空间可能也会大一些,毕竟降低hash冲突就是rehash或增加数组长度。
它提供mightContains方法,顾名思义,存在并不代表真的存在。
它还提供put方法,可用于需下层数据层新增数据时,向“布隆过滤器”中添加。但不支持删除,这是因为“布隆过滤器”原理导致,因为存在hash冲突,有可能你删除的指定key hash后位置处内容,是其他key的内容,并不是你指定key的。
当然,现在“布隆过滤器”已有很多变种,有支持删除的,它相当于提供额外的冲突计数。
二、缓存击穿
当访问的某一个key,刚好在缓存中失效了(Expire),则需要访问下层MySQL。
三、缓存雪崩
和缓存击穿类似,只不过是体量不同,当同一时刻访问的所有key同一时刻都失效了,则都需要访问下层MySQL,则极大的可能造成MySQL挂掉。
发表评论