八、redis原理之布隆过滤器实现原理

一、前言

        布隆过滤器的作用是判断一个元素是否存在于一个集合中。

        比如有一个集合存储了全国所有人的身份证号码,那么该集合大小有十几亿的大小,此时如果判断一个身份证是否存在于该集合中,最简单也是最笨的办法就是遍历集合,挨个判断是否和校验的身份证号码相同来判断。而布隆过滤器就是通过一个提高空间和时间效率的一种算法,来快速判断一个元素是否存在于集合中。

        另外还有一个问题,如果采用遍历的方式,还有一个比较大的问题就是内存的问题,假设现有一个场景,有10亿个整数,需要判断一个整数是否存在于这个整数集合中。那么首先需要创建一个int类型的数组,int类型占用4个字节也就是32位,10亿个int类型占用的空间大小就是 4*1000000000/1024/1024/1024 = 3.72G,可以算出10亿个整数存在内存中至少就需要占用3.72个G的内存,如果是20亿,就是7个G的内存,很容易就会造成内存溢出了,所以大数据的情况下,通过遍历的方式显然是不行的。此时就可以通过bitmap来实现。

1字节 = 8b

1K = 1024字节

1M = 1024K

1G = 1024M

二、bitmap

        bitmap也叫做位图,是一种数据结构。可以理解为是一个很长的数组,每一个数组元素是一个二进制值,非0即1,也就是说bitmap本质上是一个数组,每个数组元素是一个位的值。

        如果通过bitmap来存在10亿个int类型,bitmap的大小为1000000000位/8/1024/1024 = 0.12G,可以发现通过为位图来表示10亿个整数值仅仅只需要120M大小的空间就可以表示,占用的内存大小大大减少。那么接下来就了解下bitmap的结构

2.1、bitmap的结构

        int类型占用4个字节,1个字节占8位,也就是一个int类型占用32位,而bitmap是一位表示一个数字,所以可以容纳的数据是int类型的32倍。因为判断是否存在于一个集合中,只需要得到一个结果:在还是不在,那么就可以通过0和1来表示在还是不在。

如下图:

        上图中是一个int类型值在内存中的存储结构,一共占用了32位,每一位都对应了一个数字,对应的位置如果值为1,那么表示对应位置的值是存在的。如上图中可以分表表示 2、9、12、27、31的值是存在的,而这整个32位对应一个int类型的数字。相当于一个int类型的值可以表示32个数字是否存在。

所以可以用一个int类型的数组来表示一个bitmap,每个int值可以代表32个bit值。

2.2、bitmap保存数据

        将数字100保存到bitmap中,那么首先需要知道100需要存在int数组的那个位置,通过将 100/32=3可得到结果,则100位于int数字的第3个int值上

        知道了数组的那一位之后,接下来就需要设置当前数组位置上对应位的值。通过100%32=4可知位于int值的第4位,那么此时可以通过或运算进行设置 将当前位置的int值和2的4次方的值进行或运算设置结果

伪代码如下:

/** int数组表示bitmap */
static int[] bitmap = new int[];

/**
 * @param value:需要保存的值
 * */
public static void putValue(int value){
    /** 数组的每个int值可以保存32个数字, 通过除以32得到位于那个数组的位置 */
    int index = value/32;
    /** 计算偏移位置,通过对32取余数得到位于int数字的具体位置 */
    int offset = value % 32;
    /** 修改数组index位置的值,将当前的值和2的offset次方进行或运算*/
    bitmap[index] = bitmap[index] | (1<<offset);
}

        主要分成三步,第一步是计算数组的下标值,第二步是计算数组对应位置数字的第几位表示当前值,第三步是通过或运算修改当前数组位置上的值

        如现在需要判断10000个数字的bitmap,那么就需要创建10000/32长度的int数组。

第一次向bitmap中插入数字35,那么步骤如下:

        1、计算index值,35/32=1;那么对应的数值为bitmap[1]的数值,此时bitmap[1] = 0

        2、计算偏移量,35%32=3;那么表示需要在bitmap[1]的数值的第3位设置为1,此时通过将当前的值和2的3次方进行或运算,如下:

0000 0000 0000 0000 0000 0000 0000 0000 | 0000 0000 0000 0000 0000 0000 0000 1000 = 8

        3、此时bitmap[1]的值为8

        4、再次向bitmap中插入数组40,index值为 40/32 = 1;同样对应bitmap[1]的值,此时bitmap[1] = 8

        5、计算偏移量,40%32=8;那么需要将当前bitmap[1]的值和2的8次方进行或运算,如下:

0000 0000 0000 0000 0000 0000 0000 1000 | 0000 0000 0000 0000 0000 0001 0000 0000 = 0000 0000 0000 0000 0000 0001 0000 1000 = 264

        6、此时bitmap[1]的值为264

同样的插入其他值的过程如出一辙。而删除的逻辑基本一致,第一步和第二步一模一样,第三步的话是通过和对应值进行取反操作并和当前值进行与运算即可,如从bitmap中删除40。

三、布隆过滤器的实现

        由上一节可知bitmap可以实现从一个比较大的整数集合中判断一个数字是否存在,但是实际场景中往往还会有其他的场景,比如从10亿个身份证判断某个身份证号码是否存在,很显然采用bitmap就无法实现了,因为bitmap只能判断整数是否存在。所以如果有一种方式能够将身份证号码的字符串转换成一个整数,那么就可以使用bitmap来实现判断字符串是否存在于一个集合中的需求了。而通过字符串转换成整数的方式也很普遍,那么就是采用hash函数通过计算字符串的hashCode来转换成整数。

而布隆过滤器实际就是一系列的hash函数+bitmap实现的。

1、字符串存入bitmap中

        布隆过滤器是通过bitmap实现的,只不过在bitmap之上添加了多个hash函数来对传入的数据转换正常整数类型。如下图示

        字符串hello和字符串world,通过hash计算之后分别hashCode值为1和8,那么就可以通过bitmap的功能将1和8分别存入bitmap中,就相当于hello和world两个字符串存入了bitmap中。判断字符串是否存在时就可以通过计算hashCode的方式,判断对应的hash值是否存在于bitmap中即可可以判断字符串是否存在于bitmap中了

2、hash碰撞问题

        虽然通过字符串计算hash值存入bitmap中表面上没有什么问题,但是hash函数是存在一定的碰撞概率的,也就是多个字符串计算出来的hash值是一样的,此时就会出现误判的情况。

        如上图,判断字符串abcde是否存在,就需要先计算hashCode值,结果为8,此时判断结果为hashCode为8已经存在于bitmap中的,此时就会得到错误的判断是字符串abcde已经存在了,但是实际是并不存在的,而是出现了hashCode碰撞的情况。但是如果对应的hashCode在bitmap中不存在,那么就可以确认当前字符串不存在。而hashCode存在的情况下,只能说明当前字符串是可能存在。

        所以你通过布隆过滤器只能实现的功能为:能够确认一个字符串不存在于集合中,但是无法确认一个字符串存在于集合中。

 3、hash碰撞问题的优化

        由于hash函数会存在hash碰撞的情况,就导致布隆过滤器的功能会出现比较大的误差,那么既然一个hash函数存在hash碰撞,就可以采用多个hash函数来降低hash碰撞的概率。比较不同的字符串通过多个不同的hash函数还碰撞的概率会大大降低。如下图:

        字符串hello通过三个hash函数分别计算出来的hash值为1、8、25;字符串world通过三个hash函数计算出来的hash值为5、15、25,虽然hash值为25发生了hash碰撞的情况,但是两位两个hash值均没有发生hash碰撞,只有当通过三个hash函数计算出来的hash值都存在时才能够判断一个字符串可能存在,如果某个字符串通过三个hash函数计算出来的hash值只有部分存在,那么就是存在hash碰撞,且给字符串肯定不存在。

        虽然通过多个hash函数可以对于误判的情况进行优化,但是并没有本质上解决误判的情况,因为毕竟从理论上还是可能会存在多个hash值发生了hash碰撞的情况的。比如一个字符串通过三个hash函数计算的值分别为1、5、15,那么虽然和上面两个字符串都不是全部冲突了,但是1和hello发生了冲突,5和15和world发生了冲突,如果hello和world都存在,那么就会导致hash值为1、5、15的字符串产生误判的情况。

  4、布隆过滤器删除元素

      bitmap是支持删除元素的,因为bitmap不存在冲突的情况,每一个数字只会对应一个元素,而布隆过滤器的每一个元素都有可能会对应多个元素,所以不能通过删除的方式删除元素,因为这样可能会导致其他元素查询的结果不正确。

        比如上图的例子,如果将world字符串删除,那么就需要将5、15、25三个位置的值置为0,此时再判断hello是否存在结果25的位置为0,那么就导致判断结果为hello字符串不存在了。

可以通过对每一位数字计算的方式判断每一位被hash冲突了多少次来实现删除元素的方式,但是每一位增加计算就会大大增加存储的空间。

        布隆过滤器的本质是一个很长的位数组和一系列随机映射哈希函数

        布隆过滤器判断存在的数据可能存在,布隆过滤器判断不存在的数据肯定不存在;

        实际存在的数据布隆过滤器肯定判断存在,实际不存在的数据布隆过滤器可能会判断存在。

四、布隆过滤器的应用

        1、redis缓存穿透问题的解决,先将需要查询的数据存入布隆过滤器,如果布隆过滤器不存在则直接返回;如果布隆过滤器存在则再从redis查询(此时只会有少数误差数据);如果redis中还不存在则查询数据库(此时的访问很小了),并在查询数据库可以通过并发加锁处理,保证只有一个线程可以查询该数据并写入缓存,从而避免了缓存穿透的问题

        2、爬给定网址的时候对已经爬取过的URL去重

        3、邮箱的垃圾邮件过滤、黑名单等

        4、经典面试题:一个10G大小的文件,存储内容为自然数,一行一个乱序排放,需要对其进行排序操作,但是机器的内存只有2G。

        此时就可以通过布隆过滤器进行操作。首先将10G大小文件通过工具分隔成多个小文件,然后依次读取数据将数据存入bitmap中,10G的大小的自然数差不多可以存储27亿个左右的整数。

        27亿个整数存入bitmap需要占用的空间为 2700000000/8/1024/1024 = 320M左右,所以内存是足够的。然后从1到最大值进行遍历判断是否存在于bitmap中从而达到排序的效果。

五、结论

计算的数据是10W,误判的结果约等于1K,此时误判率为0.01,和我们程序设定的值一样。

问题:那是不是误判率越低越好了?

在程序里将误判率改成0.000000001,发现运算跑了好久好久…

误判率越低,需要的hash函数越多,运算越久,存储越大,消耗的性能也越高。

误判率越高,数据不准确。

如何取一个折中的值还得看具体的业务。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会飞的IT蜗牛

更美口味,打赏人生

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值