布隆过滤器(Bloom Filter)
一、简述
Bloom Filter
(布隆过滤器)是1970
年由Burton Howard Bloom
提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和不支持删除。
二、应用场景
在正式介绍Bloom Filter
算法之前,先来看看什么时候需要用到Bloom Filter
算法。
HTTP
缓存服务器、Web
爬虫等
主要工作是判断一条URL
是否在现有的URL
集合之中(可以认为这里的数据量级上亿)。
对于HTTP
缓存服务器,当本地局域网中的PC
发起一条HTTP
请求时,缓存服务器会先查看一下这个URL
是否已经存在于缓存之中,如果存在的话就没有必要去原始的服务器拉取数据了(为了简单起见,我们假设数据没有发生变化),这样既能节省流量,还能加快访问速度,以提高用户体验。
对于Web
爬虫,要判断当前正在处理的网页是否已经处理过了,同样需要当前URL
是否存在于已经处理过的URL
列表之中。
- 垃圾邮件过滤
假设邮件服务器通过发送方的邮件域或者IP
地址对垃圾邮件进行过滤,那么就需要判断当前的邮件域或者IP
地址是否处于黑名单之中。如果邮件服务器的通信邮件数量非常大(也可以认为数据量级上亿),那么也可以使用Bloom Filter
算法。
BloomFilter能解决什么问题?
以少量的内存空间判断一个元素是否属于这个集合,代价是有一定的错误率
三、工作原理
Bloom Filter
的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m
,哈希函数的个数为k
Bloom Filter
的一个例子集合S
{x
,y
,z
}。带有颜色的箭头表示元素经过k
(k
=3
)hash
函数的到在M
(bit
数组)中的位置。元素W
不在S
集合中,因为元素W
经过k
个hash
函数得到在M
(bit
数组)的k
个位置中存在值为0
的位置。
-
初始化一个数组,所有位标为
0
,A
={x1
,x2
,x3
,…,xm
} (x1
,x2
,x3
,…,xm
初始为0
) -
将已知集合
S
中的每一个数组,按以下方式映射到A
中2.0 选取
n
个互相独立的hash
函数h1
,h2
,…hk
2.1 将元素通过以上hash
函数得到一组索引值h1
(xi
),h2
(xi
),…,hk
(xi
)
2.2 将集合A
中的上述索引值标记为1(如果不同元素有重复,则重复覆盖为1,这是一个觅等操作) -
对于一个元素
x
,将其根据2.0中选取的hash
函数,进行hash
,得到一组索引值h1
(x
),h2
(x
),…,hk
(x
)如果集合A中的这些索引位置上的值都是1,表示这个元素属于集合S,否则则不属于S
3.1 前提
hash
函数的计算不能性能太差,否则得不偿失- 任意两个
hash
函数之间必须是独立的.
即任意两个hash
函数不存在单一相关性,否则hash
到其中一个索引上的元素也必定会hash
到另一个相关的索引上,这样多个hash
没有意义
3.2 错误率
工作原理的第3
步得出来的结论,一个是绝对靠谱的,一个是不能100%
靠谱的。在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive
)。因此,Bloom Filter
不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter
通过极少的错误换取了存储空间的极大节省。关于具体的错误率,这和最优的哈希函数个数以及位数组的大小有关,而这是可以估算求得一个最优解的:哈希函数个数k
、位数组大小m
及字符串数量n
之间存在相互关系。相关文献证明了对于给定的m
、n
,当\(k = ln(2)* m/n\)时出错的概率是最小的。
具体的请看:Bloom Filter概念和原理
3.3 基本特征
从以上对基本原理和数学基础的分析,我们可以得到Bloom filter
的如下基本特征,用于指导实际应用。
- 存在一定错误率,发生在正向判断上(存在性),反向判断不会发生错误(不存在性);
- 错误率是可控制的,通过改变位数组大小、
hash
函数个数或更低碰撞率的hash
函数来调节; - 保持较低的错误率,位数组空位至少保持在一半以上;
- 给定
m
和n
,可以确定最优hash
个数,即k = ln2 * (m/n)
,此时错误率最小; - 给定允许的错误率
E
,可以确定合适的位数组大小,即m >= log2(e) * (n * log2(1/E))
,继而确定hash
函数个数k
; - 正向错误率无法完全消除,即使不对位数组大小和
hash
函数个数进行限制,即无法实现零错误率; - 空间效率高,仅保存“存在状态”,但无法存储完整信息,需要其他数据结构辅助存储;
- 不支持元素删除操作,因为不能保证删除的安全性。
四、应用场景举例
-
拼写检查、数据库系统、文件系统
-
假设要你写一个网络蜘蛛(
web crawler
)。由于网络间的链接错综复杂,蜘蛛在网络间爬行很可能会形成“环”。为了避免形成“环”,就需要知道蜘蛛已经访问过那些URL
。给一个URL
,怎样知道蜘蛛是否已经访问过呢? -
网络应用
P2P
网络中查找资源操作,可以对每条网络通路保存Bloom Filter
,当命中时,则选择该通路访问。- 广播消息时,可以检测某个IP是否已发包。
- 检测广播消息包的环路,将
Bloom Filter
保存在包里,每个节点将自己添加入Bloom Filter
。 - 信息队列管理,使用
Counter Bloom Filter
管理信息流量。
-
垃圾邮件地址过滤
像网易,QQ
这样的公众电子邮件(email
)提供商,总是需要过滤来自发送垃圾邮件的人(spamer
)的垃圾邮件。一个办法就是记录下那些发垃圾邮件的email
地址。由于那些发送者不停地在注册新的地址,全世界少说也有几十亿个发垃圾邮件的地址,将他们都存起来则需要大量的网络服务器。如果用哈希表,每存储一亿个email
地址,就需要1.6GB
的内存(用哈希表实现的具体办法是将每一个email
地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有50%
,因此一个email
地址需要占用十六个字节。一亿个地址大约要1.6GB
,即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百GB
的内存。而Bloom Filter
只需要哈希表1/8
到1/4
的大小就能解决同样的问题。Bloom Filter决不会漏掉任何一个在黑名单中的可疑地址。而至于误判问题,常见的补救办法是在建立一个小的白名单,存储那些可能别误判的邮件地址。
Bloomfilter
在HBase
中的作用
HBase
利用Bloomfilter
来提高随机读(Get
)的性能,对于顺序读(Scan
)而言,设置Bloomfilter
是没有作用的(0.92以后,如果设置了bloomfilter
为ROWCOL
,对于指定了qualifier
的Scan
有一定的优化,但不是那种直接过滤文件,排除在查找范围的形式)
Bloomfilter在HBase中的开销?
Bloomfilter
是一个列族(cf
)级别的配置属性,如果你在表中设置了Bloomfilter
,那么HBase
会在生成StoreFile
时包含一份bloom filter
结构的数据,称其为MetaBlock
;MetaBlock
与DataBlock
(真实的KeyValue
数据)一起由LRUBlockCache
维护。所以,开启bloomfilter
会有一定的存储及内存cache
开销。
Bloomfilter
如何提高随机读(Get
)的性能?
对于某个region
的随机读,HBase
会遍历读memstore
及storefile
(按照一定的顺序),将结果合并返回给客户端。如果你设置了bloomfilter
,那么在遍历读storefile
时,就可以利用bloomfilter
,忽略某些storefile
。
注意:hbase
的bloom filter
是惰性加载的,在写压力比较大的情况下,会有不停的compact
并产生storefile
,那么新的storefile
是不会马上将bloom filter
加载到内存的,等到读请求来的时候才加载。这样问题就来了,第一,如果storefile
设置的比较大,max size
为2G,这会导致bloom filter
也比较大;第二,系统的读写压力都比较大。这样或许会经常出现单个GET
请求花费3-5
秒的超时现象。
五、实现
5.1 手动实现
知道了布隆过滤器的原理之后就可以自己手动实现一个,步骤如下
- 一个合适大小的位数组保存数据
- 几个不同的哈希函数
- 添加元素到位数组(布隆过滤器)的方法实现
- 判断给定元素是否存在于位数组(布隆过滤器)的方法实现。
import java.util.BitSet;
public class MyBloomFilter {
//位数组的大小
private static final int DEFAULT_SIZE = 2 << 24;
//通过这个数组可以创建 6 个不同的哈希函数
private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};
//位数组。数组中的元素只能是 0 或者 1
private BitSet bits = new BitSet(DEFAULT_SIZE);
//存放包含 hash 函数的类的数组
private SimpleHash[] func = new SimpleHash[SEEDS.length];
//初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样
public MyBloomFilter() {
// 初始化多个不同的 Hash 函数
for (int i = 0; i < SEEDS.length; i++) {
func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
}
}
//添加元素到位数组
public void add(Object value) {
for (SimpleHash f : func) {
bits.set(f.hash(value), true);
}
}
//判断指定元素是否存在于位数组
public boolean contains(Object value) {
boolean ret = true;
for (SimpleHash f : func) {
ret = ret && bits.get(f.hash(value));
}
return ret;
}
//静态内部类。用于 hash 操作!
public static class SimpleHash {
private int cap;
private int seed;
public SimpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
//计算 hash 值
public int hash(Object value) {
int h;
return (value == null)
? 0
: Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16)));
}
}
}
5.2 guava的实现
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class GuavaBloomFilter {
public static void main(String[] args) {
//创建布隆过滤器,设置存储的数据类型,预期数据量,误判率(必须大于0,小于1)
BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 100, 0.01);
//存入元素
bloomFilter.put("test");
//判断元素是否存在
System.out.println(bloomFilter.mightContain("test"));
}
}
在以上示例中,当mightContain()
方法返回true时,我们可以99%
确定该元素在过滤器中,当过滤器返回false时,我们可以100%
确定该元素不存在于过滤器中。
Guava
提供的布隆过滤器的实现还是很不错的,但是有一个重大的缺陷就是只能单机使用(容量扩展也不容易),而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到Redis
中的布隆过滤器了。
5.3 redis的实现
1.介绍
Redis v4.0
之后有了Module
(模块/插件)功能,Redis Modules
让Redis
可以使用外部模块扩展其功能。布隆过滤器就是其中的Module
。详情可以查看Redis
官方对Redis Modules
的介绍:https://redis.io/modules
另外,官网推荐了一个RedisBloom
作为Redis
布隆过滤器的Module
地址:https://github.com/RedisBloom/RedisBloom其他还有:
- redis-lua-scaling-bloom-filter(lua脚本实现):https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter
- pyreBloom(Python中的快速Redis布隆过滤器):https://github.com/seomoz/pyreBloom
- ......
RedisBloom
提供了多种语言的客户端支持,包括:Python
、Java
、JavaScript
和PHP
。
2.使用Docker安装
具体地址:https://hub.docker.com/r/redislabs/rebloom/
具体操作如下:
➜ ~ docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest
➜ ~ docker exec -it redis-redisbloom bash
root@21396d02c252:/data# redis-cli
127.0.0.1:6379> Copy to clipboardErrorCopied
3.常用命令一览
注意:
key
:布隆过滤器的名称,item
:添加的元素。
BF.ADD
:将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。格式:BF.ADD {key} {item}
。BF.MADD
:将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式BF.ADD
与之相同,只不过它允许多个输入并返回多个值。格式:BF.MADD {key} {item} [item ...]
。BF.EXISTS
:确定元素是否在布隆过滤器中存在。格式:BF.EXISTS {key} {item}
。BF.MEXISTS
:确定一个或者多个元素是否在布隆过滤器中存在格式:BF.MEXISTS {key} {item} [item ...]
。
另外,BF.RESERVE
命令需要单独介绍一下:
这个命令的格式如下:
BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion]
。
下面简单介绍一下每个参数的具体含义:
key
:布隆过滤器的名称error_rate
:误报的期望概率。这应该是介于0到1之间的十进制值。例如,对于期望的误报率0.1%
(1000中为1),error_rate
应该设置为0.001
。该数字越接近零,则每个项目的内存消耗越大,并且每个操作的CPU
使用率越高。capacity
:过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。
可选参数:
- expansion:如果创建了一个新的子过滤器,则其大小将是当前过滤器的大小乘以
expansion
。默认扩展值为2
。这意味着每个后续子过滤器将是前一个子过滤器的两倍。
4.实际使用
127.0.0.1:6379> BF.ADD myFilter java
(integer) 1
127.0.0.1:6379> BF.ADD myFilter javax
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter java
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter javax
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter github
(integer) 0