布隆过滤器详解 php-redis 布隆过滤器(在业务里可防止缓存穿透)
布隆过滤器的算法,用来判断一个元素是否在一个集合中。这种算法由一个二进制数组和一个Hash算法组成。
它的基本思路如下:
把集合中的每一个值按照提供的Hash算法算出对应的Hash值,然后将Hash值对数组长度取模后得到需要计入数组的索引值,并且将数组这个位置的值从0改成1。在判断一个元素是否存在于这个集合中时,你只需要将这个元素按照相同的算法计算出索引值,如果这个位置的值为1就认为这个元素极大可能在集合中,否则认为绝对不在集合中。
布隆过滤器特点
- 高效的插入和查询,占空间少,返回的结果是不确定性的。
- 一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。
- 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。
- 误判只会发生在过滤器没有添加过的元素,对于添加过的元素不会发生误判。
说明
通常我们会遇到很多要判断一个元素是否存在某个比较大的集合中的业务场景,一般想到的是将集合中所有元素保存起来,然后通过逐一比较确认。
链表、树、散列表(又叫哈希表,hash table)等等数据结构都是这种思路。但是正因为数据量大,存储空间也会慢慢呈线性增长,最终达到瓶颈。同时检索的速度会越来越慢,上述三种结构的检索时间复杂度分别为O(N),O(logN),O(1)。
布隆过滤器拥有极高的性能,无论是写入操作还是读取操作,时间复杂度都是O(1),是常量值。在空间上,相对于其他数据结构它也有很大的优势,比如,20亿的数组需要2000000000/8/1024/1024 = 238M的空间,而如果使用数组来存储,假设每个用户ID占用4个字节的空间,那么存储20亿用户需要2000000000 * 4 / 1024 / 1024 = 7600M的空间,是布隆过滤器的32倍。
应用场景
- 比如有50亿个电话号码,现有10万个电话号码,如何快读准确的判断这些电话号码是否存在于50亿个里?不管是把这个50亿电话号码存在与Mysql里还是内存里也好,它是非常占空间的!并且速度上Mysql也是软肋。这个时候布隆过滤器就专门可以满足此业务场景!
- 解决缓存穿透问题
- 黑名单校验
下图是布隆过滤器初始化的状态,每个坑位值都默认是0.
下图是布隆过滤器的原理,将对象通过多个hash函数转化成为哈希值或者哈希编码(也叫做列值),然后把对应的坑位值做更改。
综合上两个图而言,布隆过滤器就是一个初始值都为0的bit数组和多个哈希函数组成,用来判断某个数据是否存在。
图上为什么说是多个哈希函数呢?下面做解释
如图Tom通过hash处理之后值为56E5RD,同样Barry也一样的值,这样就出现了哈希碰撞 ,如果数据量大了,这种可能性还是比较大的,所以布隆过滤器的处理思想就是使用多个函数处理。
继续~
解决缓存穿透问题
什么是缓存穿透?
一般情况下,先查询缓存redis是否有该数据,缓存中没有的时候,再查数据库。当数据库也不存在该数据时,每次查询都要访问数据库,这就是缓存穿透。
缓存穿透的问题是,当有大量请求查询数据库不存在的数据时,就会给数据库带来压力,甚至搞垮数据库。
redis解决缓存穿透
把已存在的数据key存在布隆过滤器里,相当于redis前面挡着一个布隆过滤器。
当有新的请求时,先到布隆过滤器中查询是否存在。如果布隆过滤器中不存在该条数据则直接返回;如果布隆过滤器中已存在,才会去查询缓存redis,如果redis里没查到则穿透到mysql数据库。这样能极大的避免大量穿透,有极少数的在所难免。
黑名单过滤
假设黑名单数量是数以亿计的,存放起来就是非常消耗存储空间,布隆过滤器则是一个较好的解决方案。
把所有黑名单放在布隆过滤器中,在收到邮件的时候,先判断邮件地址是否存在布隆过滤器中即可。
布隆过滤器的操作
初始化
初始化指定大小的坑位数量,注意使用时最好不要让实际元素数量远大于初始化数量。
当实际元素数量超过初始化数量时,应该对布隆过滤器进行重建,重新分配一个size更大的过滤器,再将所有的历史元素批量add进行。
添加key
使用多个hash函数对key进行hash运算得到一个整数索引值,对位数组长度进行取模运算得到一个位置(坑位),每个hash函数都会得到一个不同的位置,讲这几个位置的值都设置为1表示完成add操作。
查询key
只要有其中一位是0,就表示这个key不存在,如果都是1,则不一定存在对应的key。
结论:
有,是可能有;无,是肯定无!
在查询的时候,如果这些点,有任何和一个为0则被查询的变量一定不存在。如果都是1,则被查询的变量可能存在!为什么说可能存在都不是一定存在呢?那是因为映射函数本身就是散列函数,散列函数是会有碰撞的,也就是哈希冲突。
比如添加了wmyskxz数据之后, 很明显图中1/3/5这三个位置的值是1,此时查询一个没添加过的字符串inexitent-key,它有可能计算后坑位也是1/3/5,这就是误判(哈希冲突)。
所以出现这种冲突的情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个bit位并不是独占,很有可能是多个元素共享某一个坑位。如果直接删除这一位的话,会影响其他元素。
php+redis实现布隆过滤器
redis客户端的布隆过滤器命令
bf.reserve filter 0.01 100 //初始化布隆过滤器名称、错误率值以及坑位大小
bf.add key 值 //如bf.add filter v1
bf.exists key 值 //如bf.exists filter v1 ,返回1;如bf.exists filter v2 ,返回0
bf.madd //一次添加多个
bf.mexists // 一次查询多个
参考文章,回头试试
https://segmentfault.com/a/1190000038989098
https://gitee.com/small_code_crown/Bloom-Filter-Demo/tree/master
https://www.cnblogs.com/bogiang/p/15165904.html
https://www.freesion.com/article/35951350402/
业务里解决缓存穿透
前面说了布隆过滤器可以解决缓存穿透问题,其实还有其他办法。
一旦发生缓存穿透,我们可以就针对查询的数据在redis中缓存一个空值或者是和业务层协商确定一个缺省值(例如库存的缺省值可以设置为0)。紧接着应用发送的后续请求再进行查询的时候,就可以直接从redis中读取空值或者缺省值,返回给业务层,避免了把大量的请求发送给数据库处理,保持了数据库正常运行。但是假如黑客拿大量你不存在的Id来请求(记得设置过期时间),否则这样会造成内存大量消耗,直至服务器宕机
综合而言还是布隆过滤器解决缓存穿透问题,看下图再次巩固