0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

布隆过滤器的实现及其他应用场景

jf_ro2CN3Fa 来源:稀土挖金技术社区 作者:小杰博士 2022-12-06 11:17 次阅读

缓存雪崩

缓存雪崩就是Redis的大量热点数据同时过期(失效),因为设置了相同的过期时间,刚好这个时候Redis请求的并发量又很大,就会导致所有的请求落到数据库。

这个问题怎么解决呢?

加互斥锁或者使用队列,针对同一个key只允许一个线程到数据库查询

缓存定时预先更新,避免同时失效

通过加随机数,使key在不同的时间过期

缓存永不过期

缓存穿透

大家看下这幅图,用户可能进行了一次条件错误的查询,这时候redis是不存在的,按照常规流程就是去数据库找了,可是这是一次错误的条件查询,数据库当然也不会存在,也不会往redis里面写值,返回给用户一个空,这样的操作一次两次还好,可是次数多了还了得,我放redis本来就是为了挡一挡,减轻数据库的压力,现在redis变成了形同虚设,每次还是去数据库查找了,这个就叫做缓存穿透,相当于redis不存在了,被击穿了,对于这种情况很好解决,我们可以在redis缓存一个空字符串或者特殊字符串,比如&&,下次我们去redis中查询的时候,当取到的值是空或者&&,我们就知道这个值在数据库中是没有的,就不会在去数据库中查询,ps:这里缓存不存在key的时候一定要设置过期时间,不然当数据库已经新增了这一条记录的时候,这样会导致缓存和数据库不一致的情况

上面这个是重复查询同一个不存在的值的情况,如果应用每次查询的不存在的值是不一样的呢?即使你每次都缓存特殊字符串也没用,因为它的值不一样,比如我们的数据库用户id是111,112,113,114依次递增,但是别人要攻击你,故意拿-100,-936,-545这种乱七八糟的key来查询,这时候redis和数据库这种值都是不存在的,人家每次拿的key也不一样,你就算缓存了也没用,这时候数据库的压力是相当大,比上面这种情况可怕的多,怎么办呢,这时候我们今天的主角布隆过滤器 就登场了。。

从一道面试题说起问:如何在海量 元素中(例如 10 亿无序、不定长、不重复)快速 判断一个元素是否存在?好,我们最简单的想法就是把这么多数据放到数据结构里去,比如List、Map、Tree,一搜不就出来了吗,比如map.get(),我们假设一个元素1个字节的字段,10亿的数据大概需要 900G 的内存空间,这个对于普通的服务器来说是承受不了的,当然面试官也不希望听到你这个答案,因为太笨了吧,我们肯定是要用一种好的方法,巧妙的方法来解决,这里引入一种节省空间的数据结构,位图 ,他是一个有序的数组,只有两个值,0 和 1。0代表不存在,1代表存在。

有了这个屌炸天的东西,现在我们还需要一个映射关系,你总得知道某个元素在哪个位置上吧,然后在去看这个位置上是0还是1,怎么解决这个问题呢,那就要用到哈希函数,用哈希函数有两个好处,第一是哈希函数无论输入值的长度是多少,得到的输出值长度是固定的,第二是他的分布是均匀的,如果全挤的一块去那还怎么区分,比如MD5、SHA-1这些就是常见的哈希算法

我们通过哈希函数计算以后就可以到相应的位置去找是否存在了,我们看红色的线,24和147经过哈希函数得到的哈希值是一样的,我们把这种情况叫做哈希冲突或者哈希碰撞 。哈希碰撞是不可避免的,我们能做的就是降低哈希碰撞的概率,第一种 是可以扩大维数组的长度或者说位图容量,因为我们的函数是分布均匀的,所以位图容量越大,在同一个位置发生哈希碰撞的概率就越小。但是越大的位图容量,意味着越多的内存消耗,所以我们想想能不能通过其他的方式来解决,第二种 方式就是经过多几个哈希函数的计算,你想啊,24和147现在经过一次计算就碰撞了,那我经过5次,10次,100次计算还能碰撞的话那真的是缘分了,你们可以在一起了,但也不是越多次哈希函数计算越好,因为这样很快就会填满位图,而且计算也是需要消耗时间,所以我们需要在时间和空间上寻求一个平衡。

布隆过滤器当然,这个事情早就有人研究过了,在 1970 年的时候,有一个叫做布隆的前辈对于判断海量元素中元素是否存在的问题进行了研究,也就是到底需要多大的位图容量和多少个哈希函数,它发表了一篇论文,提出的这个容器就叫做布隆过滤器。

大家来看下这个图,我们看集合里面3个元素,现在我们要存了,比如说a,经过f1(a),f2(a),f3(a)经过三个哈希函数的计算,在相应的位置上存入1,元素b,c也是通过这三个函数计算放入相应的位置。当取的时候,元素a通过f1(a)函数计算,发现这个位置上是1,没问题,第二个位置也是1,第三个位置上也是 1,这时候我们说这个a在布隆过滤器中是存在的,没毛病,同理我们看下面的这个d,通过三次计算发现得到的结果也都是1,那么我们能说d在布隆过滤器中是存在的吗,显然是不行的,我们仔细看d得到的三个1其实是f1(a),f1(b),f2(c)存进去的,并不是d自己存进去的,这个还是哈希碰撞导致的,我们把这种本来不存在布隆过滤器中的元素误判为存在的情况叫做假阳性(False Positive Probability,FPP)。

我们再来看另一个元素,e 元素。我们要判断它在容器里面是否存在,一样地要用这三个函数去计算。第一个位置是 1,第二个位置是 1,第三个位置是 0。那么e元素能不能判断是否在布隆过滤器中?答案是肯定的,e一定不存在。你想啊,如果e存在的话,他存进去的时候这三个位置都置为1,现在查出来有一个位置是0,证明他没存进去啊。。通过上面这张图加说明,我们得出两个重要的结论

从容器的角度来说:

如果布隆过滤器判断元素在集合中存在,不一定存在

如果布隆过滤器判断不存在,一定不存在

从元素的角度来说:

如果元素实际存在,布隆过滤器一定判断存在

如果元素实际不存在,布隆过滤器可能判断存在

小伙们请牢记

Guava实现布隆过滤器java为什么写的人多,基数大,因为是开源的,拥抱开源,框架多,轮子多,而且一个功能的轮子还不止一个,光序列化就有fastjson,jackson,gson,随你挑任你选,那布隆过滤器的轮子就是google提供的guava,我们用代码来看一下使用方法

首先引入我们的架包


com.google.guava
guava
21.0

这里先往布隆过滤器里面存放100万个元素,然后分别测试100个存在的元素和9900个不存在的元素他们的正确率和误判率

//插入多少数据
privatestaticfinalintinsertions=1000000;

//期望的误判率
privatestaticdoublefpp=0.02;

publicstaticvoidmain(String[]args){

//初始化一个存储string数据的布隆过滤器,默认误判率是0.03
BloomFilterbf=BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),insertions,fpp);

//用于存放所有实际存在的key,用于是否存在
Setsets=newHashSet(insertions);

//用于存放所有实际存在的key,用于取出
Listlists=newArrayList(insertions);

//插入随机字符串
for(inti=0;i< insertions; i++) {
            String uuid = UUID.randomUUID().toString();
            bf.put(uuid);
            sets.add(uuid);
            lists.add(uuid);
        }

        intrightNum=0;
intwrongNum=0;

for(inti=0;i< 10000;i++){
//0-10000之间,可以被100整除的数有100个(100的倍数)
Stringdata=i%100==0?lists.get(i/100):UUID.randomUUID().toString();

//这里用了might,看上去不是很自信,所以如果布隆过滤器判断存在了,我们还要去sets中实锤
if(bf.mightContain(data)){
if(sets.contains(data)){
rightNum++;
continue;
}
wrongNum++;
}
}

BigDecimalpercent=newBigDecimal(wrongNum).divide(newBigDecimal(9900),2,RoundingMode.HALF_UP);
BigDecimalbingo=newBigDecimal(9900-wrongNum).divide(newBigDecimal(9900),2,RoundingMode.HALF_UP);
System.out.println("在100W个元素中,判断100个实际存在的元素,布隆过滤器认为存在的:"+rightNum);
System.out.println("在100W个元素中,判断9900个实际不存在的元素,误认为存在的:"+wrongNum+",命中率:"+bingo+",误判率:"+percent);
}

最后得出的结果

我们看到这个结果正是印证了上面的结论,这100个真实存在元素在布隆过滤器中一定存在,另外9900个不存在的元素,布隆过滤器还是判断了216个存在,这个就是误判,原因上面也说过了,所以布隆过滤器不是万能的,但是他能帮我们抵挡掉大部分不存在的数据已经很不错了,已经减轻数据库很多压力了,另外误判率0.02是在初始化布隆过滤器的时候我们自己设的,如果不设默认是0.03,我们自己设的时候千万不能设0!

Redis实现布隆过滤器上面使用guava实现布隆过滤器是把数据放在本地内存中,我们项目往往是分布式的,我们还可以把数据放在redis中,用redis来实现布隆过滤器,这就需要我们自己设计映射函数,自己度量二进制向量的长度,下面贴代码,大家可以直接拿来用的,已经经过测试了。。

/**
*布隆过滤器核心类
*
*@param
*@authorjackxu
*/
publicclassBloomFilterHelper<T>{
privateintnumHashFunctions;
privateintbitSize;
privateFunnelfunnel;

publicBloomFilterHelper(intexpectedInsertions){
this.funnel=(Funnel)Funnels.stringFunnel(Charset.defaultCharset());
bitSize=optimalNumOfBits(expectedInsertions,0.03);
numHashFunctions=optimalNumOfHashFunctions(expectedInsertions,bitSize);
}

publicBloomFilterHelper(Funnelfunnel,intexpectedInsertions,doublefpp){
this.funnel=funnel;
bitSize=optimalNumOfBits(expectedInsertions,fpp);
numHashFunctions=optimalNumOfHashFunctions(expectedInsertions,bitSize);
}

publicint[]murmurHashOffset(Tvalue){
int[]offset=newint[numHashFunctions];

longhash64=Hashing.murmur3_128().hashObject(value,funnel).asLong();
inthash1=(int)hash64;
inthash2=(int)(hash64>>>32);
for(inti=1;i<= numHashFunctions; i++) {
            intnextHash=hash1+i*hash2;
if(nextHash< 0){
nextHash=~nextHash;
}
offset[i-1]=nextHash%bitSize;
}

returnoffset;
}

/**
*计算bit数组长度
*/
privateintoptimalNumOfBits(longn,doublep){
if(p==0){
p=Double.MIN_VALUE;
}
return(int)(-n*Math.log(p)/(Math.log(2)*Math.log(2)));
}

/**
*计算hash方法执行次数
*/
privateintoptimalNumOfHashFunctions(longn,longm){
returnMath.max(1,(int)Math.round((double)m/n*Math.log(2)));
}
}

这里在操作redis的位图bitmap,你可能只知道redis五种数据类型,string,list,hash,set,zset,没听过bitmap,但是不要紧,你可以说他是一种新的数据类型,也可以说不是,因为他的本质还是string,后面我也会专门写一篇文章来介绍数据类型以及在他们在互联网中的使用场景。。

/**
*redis操作布隆过滤器
*
*@param
*@authorxhj
*/
publicclassRedisBloomFilter<T>{
@Autowired
privateRedisTemplateredisTemplate;

/**
*删除缓存的KEY
*
*@paramkeyKEY
*/
publicvoiddelete(Stringkey){
redisTemplate.delete(key);
}

/**
*根据给定的布隆过滤器添加值,在添加一个元素的时候使用,批量添加的性能差
*
*@parambloomFilterHelper布隆过滤器对象
*@paramkeyKEY
*@paramvalue值
*@param泛型,可以传入任何类型的value
*/
publicvoidadd(BloomFilterHelperbloomFilterHelper,Stringkey,Tvalue){
int[]offset=bloomFilterHelper.murmurHashOffset(value);
for(inti:offset){
redisTemplate.opsForValue().setBit(key,i,true);
}
}

/**
*根据给定的布隆过滤器添加值,在添加一批元素的时候使用,批量添加的性能好,使用pipeline方式(如果是集群下,请使用优化后RedisPipeline的操作)
*
*@parambloomFilterHelper布隆过滤器对象
*@paramkeyKEY
*@paramvalueList值,列表
*@param泛型,可以传入任何类型的value
*/
publicvoidaddList(BloomFilterHelperbloomFilterHelper,Stringkey,ListvalueList){
redisTemplate.executePipelined(newRedisCallback(){
@Override
publicLongdoInRedis(RedisConnectionconnection)throwsDataAccessException{
connection.openPipeline();
for(Tvalue:valueList){
int[]offset=bloomFilterHelper.murmurHashOffset(value);
for(inti:offset){
connection.setBit(key.getBytes(),i,true);
}
}
returnnull;
}
});
}

/**
*根据给定的布隆过滤器判断值是否存在
*
*@parambloomFilterHelper布隆过滤器对象
*@paramkeyKEY
*@paramvalue值
*@param泛型,可以传入任何类型的value
*@return是否存在
*/
publicbooleancontains(BloomFilterHelperbloomFilterHelper,Stringkey,Tvalue){
int[]offset=bloomFilterHelper.murmurHashOffset(value);
for(inti:offset){
if(!redisTemplate.opsForValue().getBit(key,i)){
returnfalse;
}
}
returntrue;
}
}

最后就是测试类了

publicstaticvoidmain(String[]args){
RedisBloomFilterredisBloomFilter=newRedisBloomFilter();
intexpectedInsertions=1000;
doublefpp=0.1;
redisBloomFilter.delete("bloom");
BloomFilterHelperbloomFilterHelper=newBloomFilterHelper<>(Funnels.stringFunnel(Charset.defaultCharset()),expectedInsertions,fpp);
intj=0;
//添加100个元素
ListvalueList=newArrayList<>();
for(inti=0;i< 100;i++){
valueList.add(i+"");
}
longbeginTime=System.currentTimeMillis();
redisBloomFilter.addList(bloomFilterHelper,"bloom",valueList);
longcostMs=System.currentTimeMillis()-beginTime;
log.info("布隆过滤器添加{}个值,耗时:{}ms",100,costMs);
for(inti=0;i< 1000;i++){
booleanresult=redisBloomFilter.contains(bloomFilterHelper,"bloom",i+"");
if(!result){
j++;
}
}
log.info("漏掉了{}个,验证结果耗时:{}ms",j,System.currentTimeMillis()-beginTime);
}

注意这里用的是addList,他的底层是pipelining管道,而add方法的底层是一个个for循环的setBit,这样的速度效率是很慢的,但是他能有返回值,知道是否插入成功,而pipelining是不知道的,所以具体选择用哪一种方法看你的业务场景,以及需要插入的速度决定。。

布隆过滤器工作位置第一步是将数据库所有的数据加载到布隆过滤器。第二步当有请求来的时候先去布隆过滤器查询,如果bf说没有,第三步直接返回。如果bf说有,在往下走之前的流程。ps:另外guava的数据加载中只有put方法,小伙们可以想下布隆过滤器中数据删除和修改怎么办,为什么没有delete的方法?

布隆过滤器的其他应用场景网页爬虫对URL去重,避免爬取相同的 URL 地址;

反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;

Google Chrome 使用布隆过滤器识别恶意 URL;

Medium 使用布隆过滤器避免推荐给用户已经读过的文章;

Google BigTable,Apache HBbase 和 Apache Cassandra使用布隆过滤器减少对不存在的行和列的查找。

好,布隆过滤器到这里就结束了,以后在面试中面试官在问到缓存击穿怎么办,我相信你应该能够回答的头头是道了,就像我这样通俗易懂的说出来即可,然后在工作中也可以应用,比如鉴权服务,当用户登录的时候可以先用布隆过滤器判断下,而不是直接去redis、数据库查。
审核编辑:郭婷


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 数据库
    +关注

    关注

    7

    文章

    3754

    浏览量

    64253
  • 过滤器
    +关注

    关注

    1

    文章

    427

    浏览量

    19532

原文标题:通俗易懂讲布隆过滤器

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    一文理解过滤器和布谷鸟过滤器

    作者:京东保险 王奕龙 最近在大促中使用到了过滤器,所以本次借着机会整理下相关内容,并了解了布谷鸟过滤器,希望对后续学习的同学有启发~
    的头像 发表于 11-07 10:10 549次阅读
    一文理解<b class='flag-5'>布</b><b class='flag-5'>隆</b><b class='flag-5'>过滤器</b>和布谷鸟<b class='flag-5'>过滤器</b>

    乾元-呼吸过滤器气雾颗粒输出和喷雾速率测试仪-视频解说

    过滤器
    jf_91458691
    发布于 :2024年10月22日 16:42:10

    CH32FV系列CAN设备过滤器配置

    简介CAN作为总线通信协议,在总线上可能会有大量的数据包同时传输,适当的过滤机制可以提升数据处理的效率,节省处理器的资源。通过配置CAN过滤器定义自己的数据接收规则,只接收和处理符合规程的CAN数据
    的头像 发表于 10-11 08:03 183次阅读
    CH32FV系列CAN设备<b class='flag-5'>过滤器</b>配置

    优化TPS62097 Output过滤器

    电子发烧友网站提供《优化TPS62097 Output过滤器.pdf》资料免费下载
    发表于 10-08 11:19 0次下载
    优化TPS62097 Output<b class='flag-5'>过滤器</b>

    PLC工业过滤器数据采集物联网解决方案

    换热器的铜管,降低换热率,影响整个冷却系统效果。 因此往往需要加装工业过滤器以去除原料或产品中的杂质,确保产品质量。对此,物通博联提供PLC工业过滤器数据采集系统,以实现过滤器的智能化
    的头像 发表于 09-23 10:37 199次阅读
    PLC工业<b class='flag-5'>过滤器</b>数据采集物联网解决方案

    康谋分享 | ADTF过滤器全面解析:构建、配置与数据处理应用

    在ADTF(AutomotiveDataandTime-TriggeredFramework)中,过滤器(Filter)扮演着数据处理的核心角色。过滤器是处理数据流的基本单元,它们接收、处理并发
    的头像 发表于 09-18 09:42 2425次阅读
    康谋分享 | ADTF<b class='flag-5'>过滤器</b>全面解析:构建、配置与数据处理应用

    信号分析和过滤器的作用

    在通信、电子工程、计算机科学等多个领域,信号分析与过滤器扮演着举足轻重的角色。信号分析不仅涉及信号的获取、处理、分析和判断,更是从复杂的信号中提取有用信息,进行精确解读的关键过程。而过滤器,作为一种特定的信号处理技术,则在此过程中起到了不可或缺的作用。
    的头像 发表于 05-16 17:20 620次阅读

    请问STM32对过滤器编号时有哪些注意事项?

    STM32对过滤器编号时有哪些注意事项?
    发表于 04-12 08:23

    康谋技术| 揭秘汽车功能的核心——深度解读ADTF中的过滤器

    和可视化。而在ADTF软件中,过滤器图(如图1 Filter Graph所示)则是至关重要的,这也是我们理解和应用ADTF的关键所在。下面就让我们深入探索过滤器图的奥秘,揭示它在汽车功能开发中的核心作用
    的头像 发表于 02-04 11:18 509次阅读
    康谋技术| 揭秘汽车功能的核心——深度解读ADTF中的<b class='flag-5'>过滤器</b>图

    AN-B-099:DA14535 减少净排放过滤器应用说明

    电子发烧友网站提供《AN-B-099:DA14535 减少净排放过滤器应用说明.pdf》资料免费下载
    发表于 01-31 10:12 0次下载
    AN-B-099:DA14535 减少净排放<b class='flag-5'>过滤器</b>应用说明

    请问如何在CAN通信中的每个CAN消息对象中添加过滤器

    如何在CAN通信中的每个CAN消息对象中添加过滤器
    发表于 01-18 07:59

    前置过滤器的使用寿命有多长?

    前置过滤器的使用寿命有多长? 前置过滤器的使用寿命有多长,这是一个相对而言的概念,因为它的使用寿命取决于多个因素。下面将详细介绍前置过滤器的使用寿命以及影响因素。 一、什么是前置过滤器
    的头像 发表于 12-11 11:38 1083次阅读

    请问如何在AD9516-3的环状过滤器中对R和C进行冷却?

    最近我用内部 VCO 做了AD9516-3, 以排除 1G(lvprecl), 250M(lvds) 和 200M(lvds) 。 输入ref 是 200M(lvds) 。 并且 ADIsimCLK 帮助我设计循环过滤器如下:
    发表于 12-06 07:38

    springboot过滤器和拦截器哪个先执行

    Spring Boot是一个用于构建Java应用程序的开发框架,它提供了许多功能和工具来简化开发和部署过程。其中两个重要的功能是过滤器和拦截器。本文将详细介绍Spring Boot过滤器和拦截器
    的头像 发表于 12-03 15:00 2385次阅读

    程斯-空气过滤器液体细菌截留测试仪-视频解说英文

    测试仪过滤器
    sinceritysmart
    发布于 :2023年11月20日 14:21:36