当前位置: 首页 > 科技观察

冷饭新炒:了解Bloomfilter算法的实现原理

时间:2023-03-17 16:58:55 科技观察

本文将炒作一个应用广泛的算法——“Bloomfilter算法”。Bloomfilter的一些概念主要包括:算法参数优缺点介绍Bloomfilter介绍Bloomfilter是“一种空间高效的概率数据结构”(百科全书原文是一种空间高效的概率数据结构),该数据结构为BurtonHowardBloom于1970年提出,“函数是测试一个元素是否是某个集合的成员”。Bloomfilter可能会出现falsepositive(这是一个专有名词“falsepositive”,可以理解为误判情况,下面如果用到这个词,会用英文单词)。也就是说,布隆过滤器在使用的时候,有可能返回结果“可能存在于集合中”或者“一定不能存在于集合中”。BloomFilter算法描述在复杂场景的网络爬虫中,爬取的网页URL依赖关系可能形成循环。例如,URL-2显示在URL-1页面上,然后显示URL-2页面。URL-1,这时候就需要一个方案来记录和判断历史访问过的URL。这时候可能会想到以下方案:方案一:使用数据库存储访问过的URL,比如在MySQL表中根据URL建立唯一索引或者使用Redis的SET数据类型方案二:使用HashSet(其实不限于HashSet,链表、树、哈希表等数据结构都可以满足)StorevisitedURLs方案三:在方案一和方案二的基础上优化,存储URL摘要,并对URL字符串使用MD5、SHA-n算法等摘要算法摘要生成方案4:使用Hash函数对相应的URL进行处理生成哈希码,然后将哈希码映射到固定容量的某个位BitSet通过一个映射函数。对于方案1、2、3,在历史URL数据量巨大的情况下,会消耗大量的存储空间(磁盘或内存)。对于解决方案4,如果有100亿个URL,那么冲突的概率必须降低到1%,所以BitSet的容量需要设置为1万亿。因此,以上四种方案都有明显的不足,Bloomfilter算法的基本思想与方案四类似。最大的区别是方案四只提到了一个哈希函数,而装置中使用了布隆过滤器算法K(k>=1)独立的高效低冲突哈希函数。一个初始化的Bloomfilter是一个长度为m的位数组,所有位都设置为0,这就是Redis中BitArray、BitSet或BitMap的概念。那么需要引入k个不同的哈希函数。一个新元素经过这k个哈希函数处理后,映射到位数组的m位中的k位,并将这些hitmap的k位置为1,产生均匀随机分布。通常,k的一个小常数取决于所需的误报率,而Bloomfilter容量m与散列函数的数量k和要添加的元素数量正相关。将所有需要添加的元素添加到Bloomfilter后,位数组中的很多位都被设置为1。此时如果需要判断一个元素是否存在于Bloomfilter中,只需要通过k个哈希函数对位数组的k个下标进行处理,然后判断位数组下标对应的位是否为1,如果k个下标所在的位中至少有一个0,则判断该位数组下标对应的位是否为1。需要判断的一定不能在布隆过滤器代表的集合中;如果k个下标所在的位全部为1,那么这个需要判断的元素在布隆过滤器表示的集合中“可能存在”,否则恰好是FalsePositive。错误率分析见下文“Bloomfilter相关参数”一节。FalsePositive的情况如下图所示:当加入Bloomfilter的元素数量比较多,而Bloomfilter的容量设置不合理(太小)时,容易出现多个元素通过k个哈希函数,映射到相同的k位(如上图中的下标1、3、9),此时无法准确判断这k位是从具体元素映射过来的。其实你可以想得更极端一点:假设Bloomfilter容量为24,哈希函数只有一个,那么如果你加起来最多25个不同的元素,两个不同元素的映射结果一定落在同一个位.布隆过滤器的相关参数在算法描述部分已经提到。Bloomfilter主要有以下几个参数:初始化位数组容量mhash函数个数k误判率ε(数学符号Epsilon,代表FalsePositiveRate)需要加上Bloomfilter的元素个数n。考虑到篇幅,这里不做这些值的关系推导,直接整理结果和关系表达式。误报率ε的估计值为:[1-e^(-kn/m)]^k最优哈希函数数k的估计值:对于给定的m和n,当k=m/n*ln2当误判率ε最低时,计算初始比特容量m的值。当k=m/n*ln2,m>=n*log2(e)*log2(1/ε)这里参考了m/n,k和FalsePositiveRate的关系:这里可以计算空间限制表中最大的参数所需要的。假设n为10亿,m/n=32,则m为320亿,k为24,此时的误判率为2.17e-07(0.000000217),所需空间为3814.69727m。一般规律是:当k固定时,m/n越大,误判率越小。要确定准确的值,最好评估增长趋势,预先计算一个比较大的m值,以减少误判率。当然,也必须权衡m值过大带来的空间消耗过大的问题。既然参数的关系式已经有了推导结果,那么就可以根据关系式写一个参数生成器:Math.pow(1-Math.exp(Math.floorDiv(-k*n,m)),k);returnBigDecimal.valueOf(temp).setScale(10,RoundingMode.FLOOR);}publicBigDecimalkForMinFalsePositiveRate(intm,intn){BigDecimalk=BigDecimal。valueOf(Math.floorDiv(m,n)*Math.log(2));returnk.setScale(10,RoundingMode.FLOOR);}publicBigDecimalbestM(intn,doublefalsePositiveRate){doubletemp=log2(Math.exp(1)+Math.floor(1/falsePositiveRate));returnBigDecimal.valueOf(n).multiply(BigDecimal.valueOf(temp)).setScale(10,RoundingMode.FLOOR);}publicdoublelog2(doublex){returnMath.log(x)/Math.log(2);}publicstaticvoidmain(String[]args){BloomFilterParamGeneratorgenerator=newBloomFilterParamGenerator();System.out.println(generator.falsePositiveRate(2,1,2));//0.3995764008System.out.println(generator.kForMinFalsePositiveRate(32,1));//22.1807097779System.out.println(generator.bestM(1,0.3995764009));//2.2382615950}}这里的计算没有考虑严格的进位和截断,所以可能和实际结果有偏差,只提供一个参考例子Bloomfilter的优点andDisadvantagesofBloomFilters布隆过滤器的优点:相比其他数据结构,布隆过滤器在时间和空间上有巨大的优势,占用内存少,查询和插入元素的时间复杂度为O(k),可以准确判断元素Bloomfilters不存在的场景哈希函数可以独立设计Bloomfilters本身不需要存储元素,适用于一些数据敏感,数据严格保密的场景Bloomfilters的缺点:不能准确判断元素是否必须布隆过滤器中存在的场景有误判率。当k和m固定时,加入的元素越多,误判率越高。没有存储全部元素。对于一些精准查询或者精准统计的场景不适用原生Bloomfilter不能安全删除元素这里给读者一个很简单的问题:为什么原生Bloomfilter不能安全删除元素?(可以阅读之前的FalsePositive介绍)BloomfilterAlgorithmImplementation著名的Java工具库Guava自带了一个测试版的Bloomfilter实现。这里参考上面的源码实现思路和算法描述,实现一个Bloomfilter。首先考虑设计一个散列函数。更简单的方法是参考JavaBean的hashCode()方法的设计://下面的方法来自java.util.Arrays#hashCodepublicstaticinthashCode(Objecta[]){if(a==null)return0;intresult=1;for(Objectelement:a)result=31*result+(element==null?0:element.hashCode());returnresult;}上面方法的31可以作为输入种子,每个哈希函数设计一个独立的种子,并且选择种子值作为质数,根据字符串中的每个char进行叠加,实现计算结果相对独立:importjava.util.Objects;publicclassHashFunction{/***布隆过滤器容器容量*/privatefinalintm;/***seed*/privatefinalintseed;publicHashFunction(intm,intseed){this.m=m;this.seed=seed;}publicinthash(Stringelement){if(Objects.isNull(element)){return0;}intresult=1;intlen=element.length();for(inti=0;iMAX_K){thrownewIllegalArgumentException("k="+k);}this.m=m;this.bitSet=newBitSet(m);hashFunctions=newHashFunction[k];for(inti=0;icom.google.guavaGuava30.1-jre使用布隆过滤器:importcom.google.common.hash.BloomFilter;importcom.google.common.hash.Funnels;importjava.nio.charset.StandardCharsets;publicclassGuavaBloomFilter{@SuppressWarnings("UnstableApiUsage")publicstaticvoidmain(String[]args){BloomFilterbloomFilter=BloomFilter.create(Funnels.stringFunnel(StandardCharsets.US_ASCII),10000,0.0444D);bloom("Filter.throwable");bloomFilter.put("throwx");System.out.println(bloomFilter.mightContain("throwable"));System.out.println(bloomFilter.mightContain("throwx"));}}构造BloomFilter静态工厂参数最多的方法是BloomFiltercreate(Funnelfunnel,longexpectedInsertions,doublefpp,BloomFilter.Strategystrategy),参数如下:funnel:主要是将任意类型的数据转换成HashCode。它是具有大量内置实现的顶级接口。参见FunnelsexpectedInsertions:期望插入的元素个数fpp:猜测是FalsePositivePercent,误报率,小数而不是百分比,默认值0.03strategy:映射策略,目前只有MURMUR128_MITZ_32和MURMUR128_MITZ_64(默认策略)参数可以参考根据上表或参数生成器的指导,根据实际场景进行定制使用Redisson中的BloomfilterAPIRedisson高级Redis客户端Redisson已经基于Redis对位图数据结构进行了封装,屏蔽了复杂的实现逻辑,可以开箱即用。引入Redisson的依赖:org.redissonredisson3.15.1Redisson中使用BloomfilterAPI:importorg.redisson.Redisson;importorg.redisson.api.RBloomFilter;importorg.redisson.api.RedissonClient;importorg.redisson.config.Config;publicclassRedissonBloomFilter{publicstaticvoidmain(String[]args){Configconfig=newConfig();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClientredissonClient=Redisson.create(config);RBloomFilterbloomFilter=redissonClient.getBloomFilter("ipBlockList");//第一个参数expectedInsertions代表期望值插入的元素个数,第二个参数falseProbability表示预期的误报率,小数点表示bloomFilter.tryInit(100000L,0.03D);bloomFilter.add("127.0.0.1");bloomFilter.add("192.168.1.1");System.out.println(bloomFilter.contains("192.168.1.1"));//trueSystem.out.println(bloomFilter.contains("192.168.1.2"t;));//false}}Redisson提供的布隆过滤器接口RBloomFilter非常简单:常用的方法有tryInit()(初始化)、add()(添加元素)和contains()(判断元素是否存在))与Guava的内存中Bloomfilter实现相比,Redisson提供了基于Redis的“分布式Bloomfilter”,可以满足分布式集群中Bloomfilters的使用。Bloomfilter的使用场景其实Bloomfilters的使用场景可以用维基百科中的示意图来描述:基于上图列出的一些具体场景如下:网站爬虫应用中的URL去重(Bloom中不存在的URLfilter中必须是未被抓取过的URL)防火墙应用中的IP黑名单判断(不限于IP黑名单,一般的黑名单判断场景基本可以使用Bloomfilter,Bloomfilter中不存在服务器中的IP必须是awhitelist)用于避免缓存穿透(布隆过滤器中不存在的KEY在post缓存中一定不存在)布隆过滤器算法。常见的变体如下:变体名称变体描述CountingBloomFilter用一个小计数器(Counter)代替原来的Bloomfilter的每一位。所谓计数器,其实就是一个小整数。CompressedBloomFilter压缩位数组HierarchicalBloomFilters是分层的,由多层Bloomfilters组成SpectralBloomFiltersCBF扩展,提供查询集合元素出现频率的功能BloomierFilters存储函数值,而不仅仅是位映射Time-DecayingBloomFiltersCounterarrayreplacementbitVector,优化每个计数器存储其值所需的最小空间Distance-sensitiveBloomfilters-DataPopularityConsciousBloomFilters-Memory-optimizedBloomFilter-WeightedBloomfilter-SecureBloomfilters-这里选择CountingBloomFilter(简称CBF)变体稍微扩展一下。原生Bloomfilter的基本数据结构是Bitvector,CBF扩展了原生Bloomfilter的基本数据结构。底层数组的每个元素都使用一个4位的计数器来存储向数组下标添加元素时映射成功的频率。当插入一个新元素时,通过K个哈希函数映射到k个具体的计数器,这些命中计数器的值加1;当一个元素被删除时,通过k个哈希函数映射到k个具体的计数器,这些计数器的值减1。当使用CBF判断一个元素是否在集合中时:一个元素映射到k个具体的计数器通过k个哈希函数,所有计数器的值为0,则该元素一定不在集合中。一个元素通过k个哈希函数映射到k个特定的计数器,如果至少有一个计数器的值大于0,则该元素可能在集合中。总结一句话简单概括了布隆过滤器的基本功能:“不存在则一定不存在,存在则不一定存在”。在使用布隆过滤器判断一个元素是否属于某个集合时,会存在一定的误报率。也就是说,有可能将不属于某个集合的元素误判为属于这个集合。这种错误称为FalsePositive,但它不会将属于某个集合的元素误判为不属于这个集合(相对于FalsePositive,“误报”,如果属于某个集合的元素被误判为不属于这个集合的,就称为FalseNegative,“假阴性”)。FalsePositive,即错误率或误判率这个因素的引入,是布隆过滤器设计中权衡空间效率的关键。参考资料:BloomfilterGuava相关源码BloomFilters-themath(文末c-1-we-a-20210306)本文转载自微信公众号「Throwable」,可通过以下二维码关注。转载本文请联系Throwable公众号。