若是想判断一个元素是否是在一个集合里,通常想到的是将集合中全部元素保存起来,而后经过比较肯定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路,存储位置要么是磁盘,要么是内存。不少时候要么是以时间换空间,要么是以空间换时间。html
在响应时间要求比较严格的状况下,若是咱们存在内里,那么随着集合中元素的增长,咱们须要的存储空间愈来愈大,以及检索的时间愈来愈长,致使内存开销太大、时间效率变低。java
此时须要考虑解决的问题就是,在数据量比较大的状况下,既知足时间要求,又知足空间的要求。即咱们须要一个时间和空间消耗都比较小的数据结构和算法。Bloom Filter就是一种解决方案。node
布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它其实是一个很长的二进制向量和一系列随机映射函数。布隆过滤器能够用于检索一个元素是否在一个集合中。它的优势是空间效率和查询时间都远远超过通常的算法,缺点是有必定的误识别率和删除困难。redis
布隆过滤器的原理是,当一个元素被加入集合时,经过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,咱们只要看看这些点是否是都是1就(大约)知道集合中有没有它了:若是这些点有任何一个0,则被检元素必定不在;若是都是1,则被检元素极可能在。这就是布隆过滤器的基本思想。算法
Bloom Filter跟单哈希函数Bit-Map不一样之处在于:Bloom Filter使用了k个哈希函数,每一个字符串跟k个bit对应。从而下降了冲突的几率。api
bloom filter之因此能作到在时间和空间上的效率比较高,是由于牺牲了判断的准确率、删除的便利性数组
布隆过滤器有许多实现与优化,Guava中就提供了一种Bloom Filter的实现。数据结构
在使用bloom filter时,绕不过的两点是预估数据量n以及指望的误判率fpp,dom
在实现bloom filter时,绕不过的两点就是hash函数的选取以及bit数组的大小。数据结构和算法
对于一个肯定的场景,咱们预估要存的数据量为n,指望的误判率为fpp,而后须要计算咱们须要的Bit数组的大小m,以及hash函数的个数k,并选择hash函数
根据预估数据量n以及误判率fpp,bit数组大小的m的计算方式:
由预估数据量n以及bit数组长度m,能够获得一个hash函数的个数k:
哈希函数的选择对性能的影响应该是很大的,一个好的哈希函数要能近似等几率的将字符串映射到各个Bit。选择k个不一样的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,而后送入k个不一样的参数。
哈希函数个数k、位数组大小m、加入的字符串数量n的关系能够参考Bloom Filters - the math,Bloom_filter-wikipedia
看看Guava中BloomFilter中对于m和k值计算的实现,在com.google.common.hash.BloomFilter类中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
/**
* 计算 Bloom Filter的bit位数m
*
* <p>See http://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives for the
* formula.
*
* @param n 预期数据量
* @param p 误判率 (must be 0 < p < 1)
*/
@VisibleForTesting
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值,即在Bloom过滤器中插入的每一个元素的哈希数
*
* <p>See http://en.wikipedia.org/wiki/File:Bloom_filter_fp_probability.svg for the formula.
*
* @param n 预期数据量
* @param m bloom filter中总的bit位数 (must be positive)
*/
@VisibleForTesting
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
)));
}
|
BloomFilter实现的另外一个重点就是怎么利用hash函数把数据映射到bit数组中。Guava的实现是对元素经过MurmurHash3计算hash值,将获得的hash值取高8个字节以及低8个字节进行计算,以得当前元素在bit数组中对应的多个位置。MurmurHash3算法详见:Murmur哈希,于2008年被发明。这个算法hbase,redis,kafka都在使用。
这个过程的实如今两个地方:
这两个地方的实现大同小异,区别只是,前者是put数据,后者是查数据。
这里看一下put的过程,hash策略以MURMUR128_MITZ_64为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public
<T>
boolean
put(
T object, Funnel<?
super
T> funnel,
int
numHashFunctions, LockFreeBitArray bits) {
long
bitSize = bits.bitSize();
//利用MurmurHash3获得数据的hash值对应的字节数组
byte
[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
//取低8个字节、高8个字节,转成long类型
long
hash1 = lowerEight(bytes);
long
hash2 = upperEight(bytes);
boolean
bitsChanged =
false
;
//这里的combinedHash = hash1 + i * hash2
long
combinedHash = hash1;
//根据combinedHash,获得放入的元素在bit数组中的k个位置,将其置1
for
(
int
i =
0
; i < numHashFunctions; i++) {
bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);
combinedHash += hash2;
}
return
bitsChanged;
}
|
判断元素是否在bloom filter中的方法mightContain与上面的实现基本一致,再也不赘述。
简单写个demo,用法很简单,相似HashMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
package
com.qunar.sage.wang.common.bloom.filter;
import
com.google.common.base.Charsets;
import
com.google.common.hash.BloomFilter;
import
com.google.common.hash.Funnel;
import
com.google.common.hash.Funnels;
import
com.google.common.hash.PrimitiveSink;
import
lombok.AllArgsConstructor;
import
lombok.Builder;
import
lombok.Data;
import
lombok.ToString;
/**
* BloomFilterTest
*
* @author sage.wang
* @date 18-5-14 下午5:02
*/
public
class
BloomFilterTest {
public
static
void
main(String[] args) {
long
expectedInsertions =
10000000
;
double
fpp =
0.00001
;
BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), expectedInsertions, fpp);
bloomFilter.put(
"aaa"
);
bloomFilter.put(
"bbb"
);
boolean
containsString = bloomFilter.mightContain(
"aaa"
);
System.out.println(containsString);
BloomFilter<Email> emailBloomFilter = BloomFilter
.create((Funnel<Email>) (from, into) -> into.putString(from.getDomain(), Charsets.UTF_8),
expectedInsertions, fpp);
emailBloomFilter.put(
new
Email(
"sage.wang"
,
"quanr.com"
));
boolean
containsEmail = emailBloomFilter.mightContain(
new
Email(
"sage.wangaaa"
,
"quanr.com"
));
System.out.println(containsEmail);
}
@Data
@Builder
@ToString
@AllArgsConstructor
public
static
class
Email {
private
String userName;
private
String domain;
}
}
|
常见的几个应用场景: