在日常生活中,包括在設(shè)計計算機軟件時,我們經(jīng)常要判斷一個元素是否在一個集合中。比如在字處理軟件中,需要檢查一個英語單詞是否拼寫正確(也就是要判斷 它是否在已知的字典中);在 FBI,一個嫌疑人的名字是否已經(jīng)在嫌疑名單上;在網(wǎng)絡(luò)爬蟲里,一個網(wǎng)址是否被訪問過等等。
最直接的方法就是將集合中全部的元素存在計算機中,遇到一個新元素時,將它和集合中的元素直接比較即可。一般來講,計算機中的集合是用哈希表(hash table)來存儲的。它的好處是快速準確,缺點是費存儲空間(因為要同時存儲key和value,加上需要解決哈希碰撞,所以實際空間效率<50%)。當集合比較小時,這個問題不顯著,但是當集合巨大時,哈希表存儲效率低的問題就顯現(xiàn)出來了。比如說,一個象 Yahoo,Hotmail 和 Gmai 那樣的公眾電子郵件(email)提供商,總是需要過濾來自發(fā)送垃圾郵件的人(spamer)的垃圾郵件。一個辦法就是記錄下那些發(fā)垃圾郵件的 email 地址。由于那些發(fā)送者不停地在注冊新的地址,全世界少說也有幾十億個發(fā)垃圾郵件的地址,將他們都存起來則需要大量的網(wǎng)絡(luò)服務(wù)器。如果用哈希表,每存儲一億 個 email 地址, 就需要 1.6GB 的內(nèi)存(用哈希表實現(xiàn)的具體辦法是將每一個 email 地址對應(yīng)成一個八字節(jié)的信息指紋(詳見:數(shù)學(xué)之美之信息指紋), 然后將這些信息指紋存入哈希表,由于哈希表的存儲效率一般只有 50%,因此一個 email 地址需要占用十六個字節(jié)。一億個地址大約要 1.6GB, 即十六億字節(jié)的內(nèi)存)。因此存貯幾十億個郵件地址可能需要上百 GB 的內(nèi)存。除非是超級計算機,一般服務(wù)器是無法存儲的。
今天我們就介紹一個一種稱作布隆過濾器的數(shù)學(xué)工具,它只需要哈希表 1/8 到 1/4 的大小就能解決同樣的問題。
布隆過濾器介紹
原理
如果想判斷一個元素是不是在一個集合里,一般想到的是將集合中所有元素保存起來,然后通過比較確定。鏈表、樹、散列表(又叫哈希表,Hash table)等等數(shù)據(jù)結(jié)構(gòu)都是這種思路。但是隨著集合中元素的增加,我們需要的存儲空間越來越大。同時檢索速度也越來越慢。
Bloom Filter 是一種空間效率很高的隨機數(shù)據(jù)結(jié)構(gòu),Bloom filter 可以看做是對 bit-map 的擴展, 它的基本原理是:當一個元素被加入集合時,通過 K 個 Hash 函數(shù)將這個元素映射成一個位陣列(Bit array)中的 K 個點,把它們置為1。檢索時,我們只要看看這些點是不是都是1就(大約)知道集合中有沒有它了:
- 如果這些點有任何一個 0,則被檢索元素一定不在;
- 如果都是 1,則被檢索元素很可能在(存在誤判率,下文會詳解)。
如上圖可知布隆過濾器的重點有兩個:
- 一個bit數(shù)組,用來存放映射信息
- 多個Hash函數(shù),用來將元素映射到bit數(shù)組
布隆過濾器背后的數(shù)學(xué)原理在于兩個完全隨機的數(shù)學(xué)沖突峰概率很小,因此,可以在很小的無識別率的條件下,用很小的空間存儲大量的信息。
優(yōu)點
- 相比于其它的數(shù)據(jù)結(jié)構(gòu),布隆過濾器在空間和時間方面都有巨大的優(yōu)勢。布隆過濾器存儲空間和插入/查詢時間都是常數(shù)。另外, Hash 函數(shù)相互之間沒有關(guān)系,方便由硬件并行實現(xiàn)。布隆過濾器不需要存儲元素本身,在某些對保密要求非常嚴格的場合有優(yōu)勢。
- 布隆過濾器可以表示全集,其它任何數(shù)據(jù)結(jié)構(gòu)都不能。
缺點
但是布隆過濾器的缺點和優(yōu)點一樣明顯。誤算率(False Positive)是其中之一。隨著存入的元素數(shù)量增加,誤算率隨之增加(誤判補救方法是:再建立一個小的白名單,存儲那些可能被誤判的信息。)。但是如果元素數(shù)量太少,則使用散列表足矣。
另外,一般情況下不能從布隆過濾器中刪除元素. 我們很容易想到把位列陣變成整數(shù)數(shù)組,每插入一個元素相應(yīng)的計數(shù)器加1, 這樣刪除元素時將計數(shù)器減掉就可以了。然而要保證安全的刪除元素并非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器里面. 這一點單憑這個過濾器是無法保證的。另外計數(shù)器回繞也會造成問題。
布隆過濾器的應(yīng)用場景
布隆過濾器決不會漏掉任何一個在黑名單中的可疑地址。但是,它有一條不足之處。比如,它有極小的可能將一個不在黑名單中的電子郵件地址判定為在黑名單中,因為有可能某個好的郵件地址正巧對應(yīng)個八個都被設(shè)置成一的二進制位。好在這種可能性很小。我們把它稱為誤識概率。
不能接受誤報的場景
以注冊用戶的例子為例,我們利用布隆過濾器建立以注冊的用戶名單,判斷用戶是否可注冊,會按照以下步驟執(zhí)行:
- 傳入注冊用戶的通行證,根據(jù)我們建立的已注冊用戶的布隆過濾器,查詢該用戶是否存在布隆過濾器中。
- 假設(shè)該用戶不存在布隆過濾器的集合,對于元素不在集合的結(jié)果,布隆過濾器是不會誤報,所以可以放心返回該用戶可以成功注冊的結(jié)果。
- 假設(shè)用戶存在于布隆過濾器,對于元素在集合的結(jié)果,布隆過濾器有可能誤報,所以我們還需要再查詢下真正的數(shù)據(jù)庫,確認用戶是否真的已注冊了。
可以接受誤報的場景
對于垃圾郵件的黑名單過濾,它有極小的可能將一個不在黑名單中的電子郵件地址判定為在黑名單中。常見的補救辦法是在建立一個小的白名單,存儲那些可能別誤判的郵件地址。
應(yīng)用場景舉例
- chrome、360危險網(wǎng)站識別
- 垃圾郵箱識別
- 爬蟲URL去重
- 解決緩存穿透問題
- ……
布隆過濾器在java中的使用
接下來我將演示如何使用guava中封裝的布隆過濾器算法以及說明其誤判率。
創(chuàng)建代碼工程
- 新建一個maven工程,然后在pom中引入guava的依賴:
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version> //版本在18以上才提供了布隆過濾器算法
</dependency>
</dependencies>
- 新建一個測試類
BoolmFilterTest
,編寫如下代碼:
public class BoolmFilterTest {
private static final int insertions = 1000000;//100w
public static void main(String[] args) {
//初始化一個存儲String數(shù)據(jù)的布隆過濾器,初始化大小為100w
BloomFilter<String> bloomFilter =
BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions,0.01);
//初始化一個存儲String數(shù)據(jù)的set,初始化大小為100w,做驗證參考
Set<String> sets = new HashSet<String>(insertions);
//初始化一個存儲String數(shù)據(jù)額list,初始化大小為100w
List<String> lists = new ArrayList<String>(insertions);
//向三個容器中初始化100w個隨機唯一的字符串
for (int i = 0; i < insertions; i++) {
String uuid = UUID.randomUUID().toString();
bloomFilter.put(uuid);
sets.add(uuid);
lists.add(uuid);
}
//布隆過濾器誤判的次數(shù)
int wrongCount = 0;
//布隆過濾器正確地次數(shù)
int rightCount = 0;
//隨機抽取1w數(shù)據(jù)做驗證
for (int i = 0; i < 10000; i++) {
String test = i % 100 == 0 ? lists.get(i / 100) : UUID.randomUUID().toString();
//布隆過濾器驗證通過
if (bloomFilter.mightContain(test)) {
if (sets.contains(test))
rightCount++;
else
wrongCount++;
}
}
System.out.println("right count : " + rightCount);
System.out.println("wrong count : "+wrongCount);
System.out.println("wrong rate : "+Math.round(((wrongCount*1.0)/(9900))*100)+"%");
}
其中insertions
表示我們?nèi)繑?shù)據(jù)的大小(但實際bit數(shù)組的長度和hash算法的個數(shù)都是不定的,根據(jù)誤判率動態(tài)調(diào)整)。
BloomFilter<String> bloomFilter =
BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions,0.01);
這一行代碼建立一個布隆過濾器,工廠方法有三個參數(shù),分別表示過濾器的類型,容量大小,誤判率(沒錯,你可以指定誤判率,但是千萬別寫0,默認是0.03)。
接下來我們以100萬隨機字符串為數(shù)據(jù)源介紹布隆過濾器的使用以及統(tǒng)計其誤判率。
由結(jié)果可以看到布隆過濾器說不過的確實不過,但是它說過的不一定真的是對的,這就是誤判的情況,總體上實際誤判率與我們期望的是一致的。
我們再來看看算法內(nèi)部調(diào)整的數(shù)組長度和hash函數(shù)的個數(shù):
可以看到為了達到我們要求的誤判率,算法包實際創(chuàng)建的bit數(shù)組的長度是 9585058,Hash函數(shù)的個數(shù)是7個,具體為什么是這些數(shù)字,可以查看這篇論文了解:
http://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives
總結(jié)
我們首先介紹了布隆過濾器的基本原理、優(yōu)勢劣勢、使用場景;然后利用guava提供的算法包展示了字符串的判斷和測試,并驗證了它的誤判率。布隆過濾器在行業(yè)應(yīng)用比較廣泛,感謝google的工程師為我們提供了好用的算法包。