若是想判斷一個元素是否是在一個集合裏,通常想到的是將集合中全部元素保存起來,而後經過比較肯定。鏈表、樹、散列表(又叫哈希表,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;
}
}
|
常見的幾個應用場景: