BloomFilter——大规模数据处理利器
Bloom Filter是由Bloom在1970年提出的一种多哈希函数映射的快速查找算法。通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合。
( _& L3 |# _/ ]$ ?9 V# j* N0 @9 Q! `
一. 实例
为了说明Bloom Filter存在的重要意义,举一个实例:
假设要你写一个网络蜘蛛(web crawler)。由于网络间的链接错综复杂,蜘蛛在网络间爬行很可能会形成“环”。为了避免形成“环”,就需要知道蜘蛛已经访问过那些URL。给一个URL,怎样知道蜘蛛是否已经访问过呢?稍微想想,就会有如下几种方案:
1. 将访问过的URL保存到数据库。
2. 用HashSet将访问过的URL保存起来。那只需接近O(1)的代价就可以查到一个URL是否被访问过了。
3. URL经过MD5或SHA-1等单向哈希后再保存到HashSet或数据库。
4. Bit-Map方法。建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。
方法1~3都是将访问过的URL完整保存,方法4则只标记URL的一个映射位。
8 T( P/ j* ^) N- w8 [6 X& I 以上方法在数据量较小的情况下都能完美解决问题,但是当数据量变得非常庞大时问题就来了。
方法1的缺点:数据量变得非常庞大后关系型数据库查询的效率会变得很低。而且每来一个URL就启动一次数据库查询是不是太小题大做了?
方法2的缺点:太消耗内存。随着URL的增多,占用的内存会越来越多。就算只有1亿个URL,每个URL只算50个字符,就需要5GB内存。
方法3:由于字符串经过MD5处理后的信息摘要长度只有128Bit,SHA-1处理后也只有160Bit,因此方法3比方法2节省了好几倍的内存。
方法4消耗内存是相对较少的,但缺点是单一哈希函数发生冲突的概率太高。还记得数据结构课上学过的Hash表冲突的各种解决方法么?若要降低冲突发生的概率到1%,就要将BitSet的长度设置为URL个数的100倍。
实质上上面的算法都忽略了一个重要的隐含条件:允许小概率的出错,不一定要100%准确!也就是说少量url实际上没有没网络蜘蛛访问,而将它们错判为已访问的代价是很小的——大不了少抓几个网页呗。
+ q% L' X/ A) J% {
二. Bloom Filter的算法
废话说到这里,下面引入本篇的主角——Bloom Filter。其实上面方法4的思想已经很接近Bloom Filter了。方法四的致命缺点是冲突概率高,为了降低冲突的概念,Bloom Filter使用了多个哈希函数,而不是一个。
Bloom Filter算法如下:
创建一个m位BitSet,先将所有位初始化为0,然后选择k个不同的哈希函数。第i个哈希函数对字符串str哈希的结果记为h(i,str),且h(i,str)的范围是0到m-1 。
2 E! l5 g/ r+ e4 r# }* I8 G% C(1) 加入字符串过程
下面是每个字符串处理的过程,首先是将字符串str“记录”到BitSet中的过程:
对于字符串str,分别计算h(1,str),h(2,str)…… h(k,str)。然后将BitSet的第h(1,str)、h(2,str)…… h(k,str)位设为1。
图1.Bloom Filter加入字符串过程
很简单吧?这样就将字符串str映射到BitSet中的k个二进制位了。
6 Y4 w3 t0 k# D7 x0 W/ `) m(2) 检查字符串是否存在的过程
5 N9 d i3 i% N; c( ^5 }3 Q( X3 F
下面是检查字符串str是否被BitSet记录过的过程:
对于字符串str,分别计算h(1,str),h(2,str)…… h(k,str)。然后检查BitSet的第h(1,str)、h(2,str)…… h(k,str)位是否为1,若其中任何一位不为1则可以判定str一定没有被记录过。若全部位都是1,则“认为”字符串str存在。
- X/ ]! U" T1 g5 k( {4 I
若一个字符串对应的Bit不全为1,则可以肯定该字符串一定没有被Bloom Filter记录过。(这是显然的,因为字符串被记录过,其对应的二进制位肯定全部被设为1了)
但是若一个字符串对应的Bit全为1,实际上是不能100%的肯定该字符串被Bloom Filter记录过的。(因为有可能该字符串的所有位都刚好是被其他字符串所对应)这种将该字符串划分错的情况,称为false positive 。
) g. f* E* h3 g0 Q: @, f7 }
(3) 删除字符串过程
字符串加入了就被不能删除了,因为删除会影响到其他字符串。实在需要删除字符串的可以使用Counting bloomfilter(CBF),这是一种基本Bloom Filter的变体,CBF将基本Bloom Filter每一个Bit改为一个计数器,这样就可以实现删除字符串的功能了。
+ v$ X- p3 ]7 A* ]1 h' i2 T: ?6 ?6 |
Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。
l' {# L0 w0 e0 l三. Bloom Filter参数选择
5 u( U# b4 y- T- @4 J' N% R (1)哈希函数选择
哈希函数的选择对性能的影响应该是很大的,一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。
(2)Bit数组大小选择
哈希函数个数k、位数组大小m、加入的字符串数量n的关系可以参考
参考文献1。该文献证明了对于给定的m、n,当 k = ln(2)* m/n 时出错的概率是最小的。
同时该文献还给出特定的k,m,n的出错概率。例如:根据参考文献1,哈希函数个数k取10,位数组大小m设为字符串个数n的20倍时,false positive发生的概率是0.0000889 ,这个概率基本能满足网络爬虫的需求了。
! ]6 E$ U6 c5 l/ Z& e5 {
四. Bloom Filter实现代码
下面给出一个简单的Bloom Filter的Java实现代码:
1 b# o2 p. ~) X& \+ G2 |
[url=]
[/url]
, l1 }' f: Y1 U% [' [$ _import java.util.BitSet;+ |# h5 {5 t0 O
4 F5 D& `2 [2 Z: @, ppublicclass BloomFilter ( E$ Z: ?5 u# e0 g
{0 ^2 k- Z0 \( V0 ]5 P; H
/* BitSet初始分配2^24个bit */ " x, E, F- X# |% ^9 _, k! [6 i
privatestaticfinalint DEFAULT_SIZE =1<<25; 3 c+ [( Q2 | m
/* 不同哈希函数的种子,一般应取质数 */
3 v1 M3 c& q2 k! ^4 jprivatestaticfinalint[] seeds =newint[] { 5, 7, 11, 13, 31, 37, 61 };$ S( W1 Q$ i: k) l, @7 ^) Y4 X$ H
private BitSet bits =new BitSet(DEFAULT_SIZE);
) | G( b$ x- E0 a& _# U( Q+ c/* 哈希函数对象 */
; t/ X% j; |1 d; [private SimpleHash[] func =new SimpleHash[seeds.length];
0 {- ]3 Y$ j; l: m# R, M! d/ a2 [( D( T8 [
public BloomFilter()
]+ a# V2 U% p! P( c7 F1 C% k{2 ?4 M" a) B8 }$ W( u+ c# l
for (int i =0; i < seeds.length; i++)' R) }) N, R& Q2 |: F: l" H% Q- z
{
4 ]& H$ U( \7 j3 I' ^func =new SimpleHash(DEFAULT_SIZE, seeds);
8 y: D. |7 x& u4 X8 p# J7 ?9 x+ x}
6 L/ @, m% u& I0 D d}
$ B; D9 |: s' O- Y# d7 J% Q, R* |) A2 y7 Y) ~# j2 ]3 g1 Y
// 将字符串标记到bits中
: ^) t4 q+ }4 z6 g( }publicvoid add(String value)
b+ v a$ Y. ` I4 R{; _ F, S% P, w/ c
for (SimpleHash f : func)
( y1 C- D* u$ S% \{8 A/ q8 ?2 t8 n8 q& I
bits.set(f.hash(value), true);
; m8 M8 ^: e7 D9 u8 m}
4 q4 @* J/ y/ ]% b; A* J, Z6 g}8 A0 S* g0 C6 T0 \, f+ O$ V
, y, c# b: f9 t, `/ F0 C
//判断字符串是否已经被bits标记
( y& s# M- a; H' F' f$ Q. vpublicboolean contains(String value)
8 ^/ Q! [1 D. X0 R; F: m{6 _: W" m0 c! o2 h4 t+ y
if (value ==null) 3 I5 X. Q& n6 ]4 v' E4 Z
{" n, h( G) [1 Q0 O: p T
returnfalse;
+ b: @* H: { w6 z}% m- M; u! T$ A. y- B
boolean ret =true;
1 p& w H0 L$ ofor (SimpleHash f : func)
, V6 _2 k. r( K{' t7 k& y9 S/ n: m6 ?0 w) Q; K0 Q, W
ret = ret && bits.get(f.hash(value));6 s& n0 W: [, p C2 b& a; O
}) h! T; @# k8 |$ B4 y9 m" ]* _
return ret;
. m; o, y* C6 L}+ [- } ?5 `7 a2 x3 F9 M
6 F: w9 e- e3 l/* 哈希函数类 */
/ |. ~, n8 L3 S" }1 U( jpublicstaticclass SimpleHash & V, \% E+ N7 T/ @( k; V* L1 {0 g
{, m# w, X2 w% u( P
privateint cap;8 l9 c. O& @' k1 |
privateint seed;
# z* ]/ P6 ^2 a% D: d( t+ f3 T
& b" J- E+ }9 e# Fpublic SimpleHash(int cap, int seed) ) X- d p6 \$ j
{! _4 X% }# `; P& F8 _! X' {
this.cap = cap;% Z2 ~0 s0 Z4 A8 z
this.seed = seed;7 V, Z* l' I) O# m
}; j3 i/ H0 r" t: ?# U$ g
3 X7 Q" s# r: J+ ]//hash函数,采用简单的加权和hash
; B6 I1 T/ t5 dpublicint hash(String value) 0 P3 k' b% }$ T( J
{
$ Y9 I1 I0 q, m( f( vint result =0;
$ a) @, \0 @2 z) Y0 Y0 |int len = value.length();$ ~8 ~! R, N$ @$ o7 [
for (int i =0; i < len; i++)
: q9 O8 r5 F b" T7 V( z& w* j{
+ h* ]2 F1 _ g% q& s+ m, L8 ?result = seed * result + value.charAt(i);- E2 M2 o6 b9 [1 h) ]: w2 ^4 E W
}7 s/ K2 T7 a6 Q. a
return (cap -1) & result;7 x, C; z( i! X7 ?
}
5 J# u z j4 Q4 `9 x}
! x9 |: y) Q2 _4 j8 D}
' \* j8 M1 V I3 D6 C2 `) h4 A) i/ n, N+ \2 H# p9 P
[url=]
[/url]
7 y6 Y, N+ p! \" z" K; o' ~* q, }% X6 [+ ?' a) C+ o
1 ~ m- b2 D9 Q1 W2 q) z
- B8 Y( Z1 h7 d) K% }" F
) |& O+ K" U% f6 W参考文献:
/ s# c* V7 e1 C( n
[1]Pei Cao. Bloom Filters - the math.
http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
[2]Wikipedia. Bloom filter.
/ p: A5 k8 ]: V( J2 B/ j$ i5 w