文章首发于微信公众号大白话布隆过滤器,又能和面试官扯皮了~面试
近期在作推荐系统中已读内容去重的相关内容,恰好用到了布隆过滤器,因而写了一篇文章记录分享一下。redis
文章的篇幅不是很长,主要讲了布隆过滤器的核心思想,目录以下: 算法
布隆过滤器是由一个长度为m
比特的位数组与k
个哈希函数组成的数据结构。比特数组均初始化为0
,全部哈希函数均可以分别把输入数据尽可能均匀地散列。docker
当插入一个元素时,将其数据经过k
个哈希函数转换成k
个哈希值,这k
个哈希值将做为比特数组的下标
,并将数组中的对应下标的值置为1
。数据库
当查询一个元素时,一样会将其数据经过k
个哈希函数转换成k
个哈希值(数组下标),查询数组中对应下标的值,若是有一个下标的值为0
代表该元素必定不在集合中,若是所有下标的值都为1
,代表该元素有可能
在集合中。至于为何有可能在集合中? 由于有可能某个或者多个下标的值为 1 是受到其余元素的影响,这就是所谓的假阳性
,下文会详细讲述。数组
没法删除一个元素,为何呢?由于你删除的元素的哈希值可能和集合中的某个元素的哈希值有相同的,一旦删除了这个元素会致使其余的元素也被删除。缓存
下图示出一个m=18
, k=3
的布隆过滤器示例。集合中的 x、y、z 三个元素经过 3 个不一样的哈希函数散列到位数组中。当查询元素 w 时,由于有一个比特为 0,所以 w 不在该集合中。 微信
假阳性是布隆过滤器的一个痛点,所以须要不择一切手段来使假阳性的几率下降,此时就须要计算一下假阳性的几率了。数据结构
假设咱们的哈希函数选择位数组中的比特时,都是等几率的。固然在设计哈希函数时,也应该尽可能知足均匀分布。函数
在位数组长度m
的布隆过滤器中插入一个元素,它的其中一个哈希函数会将某个特定的比特置为1
。所以,在插入元素后,该比特仍然为 0 的几率是:
现有k
个哈希函数,并插入n
个元素,天然就能够获得该比特仍然为 0 的几率是:
反过来说,它已经被置为1
的几率就是:
也就是说,若是在插入n
个元素后,咱们用一个不在集合中的元素来检测,那么被误报为存在于集合中的几率(也就是全部哈希函数对应的比特都为1
的几率)为:
当n
比较大时,根据极限公式,能够近似得出假阳性率:
k
必定的状况下有以下结论:
位数组长度 m 越大,假阳性率越低。
已插入元素的个数 n 越大,假阳性率越高。
用比特数组表示,不用存储数据自己,对空间的节省相比于传统方式占据绝对的优点。
时间效率很高,不管是插入仍是查询,只须要简单的通过哈希函数转换,时间复杂度均为O(k)
。
存在假阳性
的几率,准确率要求高的场景不太适用。
只能插入和查询,不能删除了元素。
布隆过滤器的用途不少,可是主要的做用就是去重,这里列举几个使用场景。
试想一下,百度是一个爬虫,它会定时搜集各大网站的信息,文章,那么它是如何保证爬取到文章信息不重复,它会将 URL 存放到布隆过滤器中,每次爬取以前先从布隆过滤器中判断这个 URL 是否存在,这样就避免了重复爬取。固然这种存在假阳性的可能,可是只要你的比特数组足够大,假阳性
的几率会很低,另外一方面,你认为百度会在乎这种的偏差吗,你的一篇文章可能由于假阳性几率没有收录到,对百度有影响吗?
读者朋友们应该没人没刷过抖音吧,每次刷的时候抖音给你的视频有重复的吗?他是如何保证推荐的内容不重复的呢?
最容易想到的就是抖音会记录用户的历史观看记录,而后从历史记录中排除。这是一种解决办法,可是性能呢?不用多说了,有点常识的都知道这不可能。
解决这种重复的问题,布隆过滤器有着绝对的优点,可以很轻松的解决。
缓存穿透是指查询一条数据库和缓存都没有的一条数据,就会一直查询数据库,对数据库的访问压力会一直增大。
布隆过滤器在解决缓存穿透的问题效果也是很好,这里再也不细说,后续文章会写。
了解布隆过滤器的设计思想以后,想要实现一个布隆过滤器其实很简单,陈某这里就再也不搬门弄斧了,介绍一下现成的实现方式吧。
Redis4.0 以后推出了插件的功能,下面用 docker 安装:
docker pull redislabs/rebloom docker run -p6379:6379 redislabs/rebloom
安装完成后链接 redis 便可,运行命令:
redis-cli
至于具体的使用这里再也不演示了,直接看官方文档和教程,使用起来仍是很简单的。
guava 对应布隆过滤器的实现作出了支持,使用 guava 能够很轻松的实现一个布隆过滤器。
1. 建立布隆过滤器
建立布隆过滤器,以下:
BloomFilter<Integer> filter = BloomFilter.create( Funnels.integerFunnel(), 5000, 0.01); //插入 IntStream.range(0, 100_000).forEach(filter::put); //判断是否存在 boolean b = filter.mightContain(1);
arg1
:用于将任意类型 T 的输入数据转化为 Java 基本类型的数据,这里转换为 byte
arg2
:byte 字节数组的基数
arg3
:指望的假阳性几率
2.估计最优 m 值和 k 值
guava 在底层对 byte 数组的基数(m)和哈希函数的个数 k 作了本身的算法,源码以下:
//m值的计算 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))); } //k值的计算 static int optimalNumOfHashFunctions(long n, long m) { // (m / n) * log(2), but avoid truncation due to division! return Math.max(1, (int) Math.round((double) m / n * Math.log(2))); }
想要理解 guava 的计算原理,还要从的上面推导的过程继续。
由假阳性率的近似计算方法可知,若是要使假阳性率尽可能小,在 m 和 n 给定的状况下,k
值应为:
将 k 代入上一节的式子并化简,咱们能够整理出指望假阳性率 p 与 m、n 的关系:
换算而得:
若是指按期望假阳性率 p,那么最优的 m 值与指望元素数 n 呈线性关系。
最优的 k 值实际上只与 p 有关,与 m 和 n 都无关,即:
综上两个结论,在建立布隆过滤器的时候,肯定p
值和m
值很重要。
至此,布隆过滤器的知识介绍到这里,若是以为陈某写得不错的,转发在看点一波,读者的一份支持将会是我莫大的鼓励。
另外陈某为你们准备了500份私藏电子书和大量视频教程,嘘,私密哟,关注微信公众号便可获取!!