本文是站在小白的角度去讨论布隆过滤器,若是你是科班出身,或者比较聪明,又或者真正想彻底搞懂布隆过滤器的能够移步。redis
不知道从何时开始,原本默默无闻的布隆过滤器一会儿名声大燥,仿佛身在互联网,作着开发的,无人不知,无人不晓,哪怕对技术不是很关心的小伙伴也听过它的名号。我也花了很多时间去研究布隆过滤器,看了很多博客,无奈不是科班出身,又没有那么聪明的头脑,又比较懒...通过“放弃,拿起,放弃,拿起”的无限轮回,应该算是了解了布隆过滤器的核心思想,因此想给你们分享下。数据库
咱们先来看下布隆过滤器的应用场景,让你们知道神奇的布隆过滤器到底能作什么。数组
咱们常常会把一部分数据放在Redis等缓存,好比产品详情。这样有查询请求进来,咱们能够根据产品Id直接去缓存中取数据,而不用读取数据库,这是提高性能最简单,最广泛,也是最有效的作法。通常的查询请求流程是这样的:先查缓存,有缓存的话直接返回,若是缓存中没有,再去数据库查询,而后再把数据库取出来的数据放入缓存,一切看起来很美好。可是若是如今有大量请求进来,并且都在请求一个不存在的产品Id,会发生什么?既然产品Id都不存在,那么确定没有缓存,没有缓存,那么大量的请求都怼到数据库,数据库的压力一会儿就上来了,还有可能把数据库打死。
虽然有不少办法均可以解决这问题,可是咱们的主角是“布隆过滤器”,没错,“布隆过滤器”就能够解决(缓解)缓存穿透问题。至于为何说是“缓解”,看下去你就明白了。缓存
如今有大量的数据,而这些数据的大小已经远远超出了服务器的内存,如今再给你一个数据,如何判断给你的数据在不在其中。若是服务器的内存足够大,那么用HashMap是一个不错的解决方案,理论上的时间复杂度能够达到O(1),可是如今数据的大小已经远远超出了服务器的内存,因此没法使用HashMap,这个时候就可使用“布隆过滤器”来解决这个问题。可是仍是一样的,会有必定的“误判率”。服务器
布隆过滤器是一个叫“布隆”的人提出的,它自己是一个很长的二进制向量,既然是二进制的向量,那么显而易见的,存放的不是0,就是1。数据结构
如今咱们新建一个长度为16的布隆过滤器,默认值都是0,就像下面这样:
函数
如今须要添加一个数据:性能
咱们经过某种计算方式,好比Hash1,计算出了Hash1(数据)=5,咱们就把下标为5的格子改为1,就像下面这样:测试
咱们又经过某种计算方式,好比Hash2,计算出了Hash2(数据)=9,咱们就把下标为9的格子改为1,就像下面这样:
搜索引擎
仍是经过某种计算方式,好比Hash3,计算出了Hash3(数据)=2,咱们就把下标为2的格子改为1,就像下面这样:
这样,刚才添加的数据就占据了布隆过滤器“5”,“9”,“2”三个格子。
能够看出,仅仅从布隆过滤器自己而言,根本没有存放完整的数据,只是运用一系列随机映射函数计算出位置,而后填充二进制向量。
这有什么用呢?好比如今再给你一个数据,你要判断这个数据是否重复,你怎么作?
你只需利用上面的三种固定的计算方式,计算出这个数据占据哪些格子,而后看看这些格子里面放置的是否都是1,若是有一个格子不为1,那么就表明这个数字不在其中。这很好理解吧,好比如今又给你了刚才你添加进去的数据,你经过三种固定的计算方式,算出的结果确定和上面的是如出一辙的,也是占据了布隆过滤器“5”,“9”,“2”三个格子。
可是有一个问题须要注意,若是这些格子里面放置的都是1,不必定表明给定的数据必定重复,也许其余数据通过三种固定的计算方式算出来的结果也是相同的。这也很好理解吧,好比咱们须要判断对象是否相等,是不能够仅仅判断他们的哈希值是否相等的。
也就是说布隆过滤器只能判断数据是否必定不存在,而没法判断数据是否必定存在。
按理来讲,介绍完了新增、查询的流程,就要介绍删除的流程了,可是很遗憾的是布隆过滤器是很难作到删除数据的,为何?你想一想,好比你要删除刚才给你的数据,你把“5”,“9”,“2”三个格子都改为了0,可是可能其余的数据也映射到了“5”,“9”,“2”三个格子啊,这不就乱套了吗?
相信通过我这么一介绍,你们对布隆过滤器应该有一个浅显的认识了,至少你应该清楚布隆过滤器的优缺点了:
能够看到,布隆过滤器的优势和缺点同样明显。
在上文中,我举的例子二进制向量长度为16,由三个随机映射函数计算位置,在实际开发中,若是你要添加大量的数据,仅仅16位是远远不够的,为了让误判率下降,咱们还能够用更多的随机映射函数、更长的二进制向量去计算位置。
如今相信你对布隆过滤器应该有一个比较感性的认识了,布隆过滤器核心思想其实并不难,难的在于如何设计随机映射函数,到底映射几回,二进制向量的长度设置为多少比较好,这可能就不是通常的开发能够驾驭的了,好在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次,咱们计算下误判率
和咱们定义的指望误判率0.01相差无几。
上面使用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可能重复
本篇博客到这里就结束了,谢谢你们。