牛逼哄哄的布隆过滤器,到底有什么用?

简介: 本文是站在小白的角度去讨论布隆过滤器,如果你是科班出身,或者比较聪明,又或者真正想完全搞懂布隆过滤器的可以移步。不知道从什么时候开始,本来默默无闻的布隆过滤器一下子名声大燥,仿佛身在互联网,做着开发的,无人不知,无人不晓,哪怕对技术不是很关心的小伙伴也听过它的名号。

本文是站在小白的角度去讨论布隆过滤器,如果你是科班出身,或者比较聪明,又或者真正想完全搞懂布隆过滤器的可以移步。


不知道从什么时候开始,本来默默无闻的布隆过滤器一下子名声大燥,仿佛身在互联网,做着开发的,无人不知,无人不晓,哪怕对技术不是很关心的小伙伴也听过它的名号。


我也花了不少时间去研究布隆过滤器,看了不少博客,无奈不是科班出身,又没有那么聪明的头脑,又比较懒...经过“放弃,拿起,放弃,拿起”的无限轮回,应该算是了解了布隆过滤器的核心思想,所以想给大家分享下。


布隆过滤器的应用


我们先来看下布隆过滤器的应用场景,让大家知道神奇的布隆过滤器到底能做什么。


缓存穿透

我们经常会把一部分数据放在Redis等缓存,比如产品详情。这样有查询请求进来,我们可以根据产品Id直接去缓存中取数据,而不用读取数据库,这是提升性能最简单,最普遍,也是最有效的做法。面试常问,缓存三大问题及解决方案!


一般的查询请求流程是这样的:先查缓存,有缓存的话直接返回,如果缓存中没有,再去数据库查询,然后再把数据库取出来的数据放入缓存,一切看起来很美好。


但是如果现在有大量请求进来,而且都在请求一个不存在的产品Id,会发生什么?既然产品Id都不存在,那么肯定没有缓存,没有缓存,那么大量的请求都怼到数据库,数据库的压力一下子就上来了,还有可能把数据库打死。


虽然有很多办法都可以解决这问题,但是我们的主角是“布隆过滤器”,没错,“布隆过滤器”就可以解决(缓解)缓存穿透问题。至于为什么说是“缓解”,看下去你就明白了。


大量数据,判断给定的是否在其中

现在有大量的数据,而这些数据的大小已经远远超出了服务器的内存,现在再给你一个数据,如何判断给你的数据在不在其中。


如果服务器的内存足够大,那么用HashMap是一个不错的解决方案,理论上的时间复杂度可以达到O(1),但是现在数据的大小已经远远超出了服务器的内存,所以无法使用HashMap,这个时候就可以使用“布隆过滤器”来解决这个问题。但是还是同样的,会有一定的“误判率”。


什么是布隆过滤器


布隆过滤器是一个叫“布隆”的人提出的,它本身是一个很长的二进制向量,既然是二进制的向量,那么显而易见的,存放的不是0,就是1。


现在我们新建一个长度为16的布隆过滤器,默认值都是0,就像下面这样:

image.png


现在需要添加一个数据:


我们通过某种计算方式,比如Hash1,计算出了Hash1(数据)=5,我们就把下标为5的格子改成1,就像下面这样:


image.png


我们又通过某种计算方式,比如Hash2,计算出了Hash2(数据)=9,我们就把下标为9的格子改成1,就像下面这样:


image.png

还是通过某种计算方式,比如Hash3,计算出了Hash3(数据)=2,我们就把下标为2的格子改成1,就像下面这样:


image.png

这样,刚才添加的数据就占据了布隆过滤器“5”,“9”,“2”三个格子。


可以看出,仅仅从布隆过滤器本身而言,根本没有存放完整的数据,只是运用一系列随机映射函数计算出位置,然后填充二进制向量。


这有什么用呢?比如现在再给你一个数据,你要判断这个数据是否重复,你怎么做?


你只需利用上面的三种固定的计算方式,计算出这个数据占据哪些格子,然后看看这些格子里面放置的是否都是1,如果有一个格子不为1,那么就代表这个数字不在其中。


这很好理解吧,比如现在又给你了刚才你添加进去的数据,你通过三种固定的计算方式,算出的结果肯定和上面的是一模一样的,也是占据了布隆过滤器“5”,“9”,“2”三个格子。


但是有一个问题需要注意,如果这些格子里面放置的都是1,不一定代表给定的数据一定重复,也许其他数据经过三种固定的计算方式算出来的结果也是相同的。这也很好理解吧,比如我们需要判断对象是否相等,是不可以仅仅判断他们的哈希值是否相等的。


也就是说布隆过滤器只能判断数据是否一定不存在,而无法判断数据是否一定存在。


按理来说,介绍完了新增、查询的流程,就要介绍删除的流程了,但是很遗憾的是布隆过滤器是很难做到删除数据的,为什么?你想想,比如你要删除刚才给你的数据,你把“5”,“9”,“2”三个格子都改成了0,但是可能其他的数据也映射到了“5”,“9”,“2”三个格子啊,这不就乱套了吗?


相信经过我这么一介绍,大家对布隆过滤器应该有一个浅显的认识了,至少你应该清楚布隆过滤器的优缺点了:


优点:由于存放的不是完整的数据,所以占用的内存很少,而且新增,查询速度够快;


缺点:随着数据的增加,误判率随之增加;无法做到删除数据;只能判断数据是否一定不存在,而无法判断数据是否一定存在。


可以看到,布隆过滤器的优点和缺点一样明显。


在上文中,我举的例子二进制向量长度为16,由三个随机映射函数计算位置,在实际开发中,如果你要添加大量的数据,仅仅16位是远远不够的,为了让误判率降低,我们还可以用更多的随机映射函数、更长的二进制向量去计算位置。


guava实现布隆过滤器


现在相信你对布隆过滤器应该有一个比较感性的认识了,布隆过滤器核心思想其实并不难,难的在于如何设计随机映射函数,到底映射几次,二进制向量的长度设置为多少比较好,这可能就不是一般的开发可以驾驭的了。


好在Google大佬给我们提供了开箱即用的组件,来帮助我们实现布隆过滤器,现在就让我们看看怎么Google大佬送给我们的“礼物”吧。


首先在pom引入“礼物”:

<dependency>  
  <groupId>com.google.guava</groupId>  
  <artifactId>guava</artifactId>  
  <version>19.0</version>  
</dependency>

然后就可以测试啦:

private static int size = 1000000;//预计要插入多少数据  
private static double fpp = 0.01;//期望的误判率  
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);  
public static void main(String[] args) {  
  //插入数据  
  for (int i = 0; i < 1000000; i++) {  
    bloomFilter.put(i);  
  }  
  int count = 0;  
  for (int i = 1000000; i < 2000000; i++) {  
    if (bloomFilter.mightContain(i)) {  
      count++;  
      System.out.println(i + "误判了");  
    }  
  }  
  System.out.println("总共的误判数:" + count);  
}

代码简单分析:

我们定义了一个布隆过滤器,有两个重要的参数,分别是 我们预计要插入多少数据,我们所期望的误判率,误判率不能为0。

我向布隆过滤器插入了0-1000000,然后用1000000-2000000来测试误判率。

运行结果:

1999501误判了  
1999567误判了  
1999640误判了  
1999697误判了  
1999827误判了  
1999942误判了  
总共的误判数:10314

现在总共有100万数据是不存在的,误判了10314次,我们计算下误判率:


image.png


和我们定义的期望误判率0.01相差无几。


redis实现布隆过滤器


上面使用guava实现布隆过滤器是把数据放在本地内存中,无法实现布隆过滤器的共享,我们还可以把数据放在redis中,用 redis来实现布隆过滤器,我们要使用的数据结构是bitmap,你可能会有疑问,redis支持五种数据结构:String,List,Hash,Set,ZSet,没有bitmap呀。没错,实际上bitmap的本质还是String。


可能有小伙伴会说,纳尼,布隆过滤器还没介绍完,怎么又出来一个bitmap,没事,你可以把bitmap就理解为一个二进制向量。


要用redis来实现布隆过滤器,我们需要自己设计映射函数,自己度量二进制向量的长度,这对我来说,无疑是一个不可能完成的任务,只能借助搜索引擎,下面直接放出代码把。

public class RedisMain {  
    static final int expectedInsertions = 100;//要插入多少数据  
    static final double fpp = 0.01;//期望的误判率  
    //bit数组长度  
    private static long numBits;  
    //hash函数数量  
    private static int numHashFunctions;  
    static {  
        numBits = optimalNumOfBits(expectedInsertions, fpp);  
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);  
    }  
    public static void main(String[] args) {  
        Jedis jedis = new Jedis("192.168.0.109", 6379);  
        for (int i = 0; i < 100; i++) {  
            long[] indexs = getIndexs(String.valueOf(i));  
            for (long index : indexs) {  
                jedis.setbit("codebear:bloom", index, true);  
            }  
        }  
        for (int i = 0; i < 100; i++) {  
            long[] indexs = getIndexs(String.valueOf(i));  
            for (long index : indexs) {  
                Boolean isContain = jedis.getbit("codebear:bloom", index);  
                if (!isContain) {  
                    System.out.println(i + "肯定没有重复");  
                }  
            }  
            System.out.println(i + "可能重复");  
        }  
    }  
    /**  
     * 根据key获取bitmap下标  
     */  
    private static long[] getIndexs(String key) {  
        long hash1 = hash(key);  
        long hash2 = hash1 >>> 16;  
        long[] result = new long[numHashFunctions];  
        for (int i = 0; i < numHashFunctions; i++) {  
            long combinedHash = hash1 + i * hash2;  
            if (combinedHash < 0) {  
                combinedHash = ~combinedHash;  
            }  
            result[i] = combinedHash % numBits;  
        }  
        return result;  
    }  
    private static long hash(String key) {  
        Charset charset = Charset.forName("UTF-8");  
        return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();  
    }  
    //计算hash函数个数  
    private static int optimalNumOfHashFunctions(long n, long m) {  
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));  
    }  
    //计算bit数组长度  
    private static long optimalNumOfBits(long n, double p) {  
        if (p == 0) {  
            p = Double.MIN_VALUE;  
        }  
        return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));  
    }  
}

运行结果:

88可能重复  
89可能重复  
90可能重复  
91可能重复  
92可能重复  
93可能重复  
94可能重复  
95可能重复  
96可能重复  
97可能重复  
98可能重复  
99可能重复

本篇博客到这里就结束了,谢谢大家。写作不易,坚持更难,如大家喜欢就帮忙推送给其他人!

相关文章
|
负载均衡 Java API
【Spring Cloud Gateway 新一代网关】—— 每天一点小知识
【Spring Cloud Gateway 新一代网关】—— 每天一点小知识
519 0
|
JSON 数据挖掘 API
京东商品评论数据接口:洞察消费者心声的重要渠道
京东商品评论数据接口提供了商品用户评价信息,包括评价内容、时间、星级、用户头像、昵称、图片和视频地址等。使用时需注册京东开放平台账号,获取认证信息,查阅API文档,明确所需商品信息并调用接口,解析返回的JSON数据以获取评论。此接口适用于市场分析、产品改进、提升用户体验、品牌塑造与口碑营销以及电商运营决策等多个场景,帮助企业深入了解消费者需求,优化产品和服务。
|
自然语言处理 算法 BI
Baum-Welch算法
Baum-Welch算法
|
存储 JSON 数据处理
分析、总结Python使用列表、元组、字典的场景
分析、总结Python使用列表、元组、字典的场景
436 0
|
存储 NoSQL 容灾
怎样保证Redis 保证数据不丢失?
Redis 数据不丢失主要靠持久化(RDB、AOF、混合)和集群运行(主从同步、哨兵、Cluster)。RDB是快照,恢复速度快但可能丢失部分数据;AOF记录所有命令,实时性好但写性能较低;混合持久化结合两者优点。集群通过多服务器分布数据,提高可用性和数据安全性。
586 5
|
Prometheus 监控 Cloud Native
ChaosBlade接入问题之资源监控接入如何解决
ChaosBlade 是一个开源的混沌工程实验工具,旨在通过模拟各种常见的硬件、软件、网络、应用等故障,帮助开发者在测试环境中验证系统的容错和自动恢复能力。以下是关于ChaosBlade的一些常见问题合集:
|
运维 关系型数据库 MySQL
MySQL DBA的必备参考,两位数据库资深专家呕心沥血之作
互联网发展至今,开源软件已经深入人心,并且受到广泛的支持和响应,很多公司在使用开源软件的同时也输出了一些好的开源产品。MySQL 作为当今世界.上最受欢迎的开源数据库产品之一,在很多互联网企业里起到了不可或缺的作用。由于MySQL的诸多特性,比如开源免费、灵活、轻量简单且越来越多的企业开始使用MySQL,在业界诞生了一大批相关从业者,他们研究MySQL的原理,探讨MySQL的架构,完善MySQL的运维,丰富MySQL的工具,促进MySQL的发展,我们称这些人为MySQL DBA,而本人也是其中之一,深感荣幸。
|
程序员 Python
Python Qt GUI设计:窗口之间数据传递(拓展篇—5)
在开发程序时,如果这个程序只有一个窗口,则应该关心这个窗口里面的各个控件之间是如何传递数据的。如果这个程序有多个窗口,那么还应该关心不同的窗口之间是如何传递数据的。 本篇博文首先给出一个例子,说明在一个窗口中不同控件之间的数据是如何传递的。对于多窗口的情况,一般有两种解决方法:一种是主窗口获取子窗口中控件的属性,另一种是通过信号与槽机制,一般是子窗口通过发射信号的形式传递数据,主窗口的槽函数获取这些数据。
|
存储 编译器
西门子S7-200 SMART数据块的使用
今天我们来学习在西门子S7-200 SMART中如何使用数据块。在讲解数据块的使用之前我们先来看一下什么是数据块:数据块用来对V存储区也叫变量存储区赋初始值;可以对字节、字或双字来分配数据值。
西门子S7-200 SMART数据块的使用

热门文章

最新文章