1 位圖(BitMap)
在討論布隆過濾器之前,先看一下位圖是什么。
首先考慮一個問題場景
假如需要過濾某些不安全網頁,現有100億個黑名單頁面,每個網頁的URL最多占用64字節。現要設計一種網頁過濾系統,可以根據網頁的URL判斷該網頁是否在黑名單上。
最直觀的想法必然是使用一個集合或者說數據結構來存放黑名單URL,比如查找樹、Set、map,但是無論哪種,不可避免的是我們需要存儲原始的URL值,但是我們都知道URL並不是一個很短的字符串,動輒十幾/幾十字節,假設一個URL有30字節那么100億URL所占內存就是十幾G,所以每次判斷是否存在於黑名單中,就要占用很大的內存開銷。
但是,我們需要的僅僅是知道是否存在這一需求,可以不需要具體的URL,所以僅僅對Ture or False 這個問題,可以使用位圖(BitMap)算法,位圖顧名思義就是,每個map值都使用1bit,這樣大大降低了內存開銷,具體做法是,我們使用一個Hash函數將URL映射到大小為n的bit數組中,並置相應位置為True
這樣我們可以在盡可能低的內存開銷下,實現O(1)時間的判斷URL是否存在黑名單中。
但不得不面對的一個問題就是,即使采取再好的哈希函數,都會出現哈希沖突的情況,在查詢階段出現哈希沖突意味着查詢錯誤,會返回一個錯誤的結果,而想盡可能的降低哈希沖突,我們需要位圖大小比黑名單中URL數量大的多,我們考慮隨機哈希的情況下,查詢碰撞的概率是:黑名單URL數量/位圖大小。所以要想查詢准確率高,又帶來了更高的內存開銷,而可以有效改善這種情況的一種數據結構叫做布隆過濾器(Bloom Filter)。
2 布隆過濾器(Bloom Filter)
2.1 是什么
考慮位圖情況出現的問題:在有限的比特數組大小下,碰撞概率會很高,布隆過濾器解改善了這個問題,具體的,它使用多個Hash函數對數據進行哈希操作(如下圖使用了兩個hash函數),這樣得出多個位置為True,相比位圖它在有限的空間內,盡可能的降低了查詢失敗的可能,這一點可以從信息熵的角度來看,每一個位置所包含的信息更加多了,所以比起位圖來說,布隆過濾器對空間的利用率也變大了,當然這一點也會帶來別的壞處下面會說到。
以上圖為例,假設對bilibli.com
進行兩次hash運算
得到結果后,令BitSet數組中下標為4和6的位置1,同樣對cnblogs.com
進行兩次hash計算映射到數組中下標0和4的位置,置1,那么假如來一條查詢信息,只需要同樣計算兩次哈希,若同時為1,則返回true即可。
而實際操作中,哈希函數一般會選取多個,比如常用的8個哈希函數,盡可能的在有限的空間內降低查詢出現哈希沖突的可能,但是沖突現象顯然是無法避免的,只能根據需求,通過合理的選擇位數組的大小以及哈希函數來盡可能降低沖突率。
上面談到,布隆過濾器每個位置不再標識一條數據,可能標識多條數據,因而對於bit數組中的某個位置來講,它的值信息熵更大了,但是既然一個位置可以標識多條數據,正所謂牽一發而動全身,所以布隆過濾器也就存在的一個問題,無法對黑名單進行刪除操作,比如上述的例子,下標為4的位置是兩條數據經過不同的哈希運算后得到了同樣的結果,假如要刪除一條數據必然影響其他數據的查詢結果,造成更高的誤判率。
2.2 誤判率
布隆過濾器的誤判率也可以稱之為假陽性(false positive)的概率,比如來一條URL查詢是否在黑名單中,結果其對應的哈希結果已經被其他一個或者多個URL置1,那么此時就出現了查詢錯誤的情況。所以布隆過濾器只適合有內存開銷限制、並且允許出現錯誤率的情況,我們可以通過分析其出現錯誤的概率,選取合適的bit數組大小以及哈希函數,盡可能在內存開銷和錯誤率中間進行一個折中的選擇,下面分析一下布隆過濾器的誤判率。
假設bit數組大小為m,數據量為n,使用k個不同的哈希函數映射,那么考慮隨機哈希情況下,數組中某一個位置在一次哈希后被置1的概率為
那么在一次哈希過后,數組中某一位為0的概率即為
經過k個哈希函數后,某一個位置為0的概率即為
考慮m足夠大的情況下有
所以有k個哈希函數后,某一個位置為0的概率即為
則插入n個數后,某個位置仍然為0的概率為
所以插入n個數后,bit數組中某個位置為1的概率為
所以在插入n個數后,來一條查詢數據,數據經過k個哈希函數映射后,bit數組中k個位置均為1的概率為:
P即為布隆過濾器,將n條數據,進行k次哈希后,存入大小為m的bit數組后,再查詢一條數據,出現誤判(數據不存在,卻誤以為存在)的概率。
所以根據這個概率,在考慮設計布隆過濾器時,假如已給定大致的數據總量n,我們就可以通過調整m和k的大小來盡可能的得到較低的誤判率也就是概率P,下面給出部分概率參考。(表格參考 http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html ,里面也有簡單的概率推導以及更加詳細的表格)
觀察上表可以看出,假設m/n為16的情況下,即每條數據使用16bit大小的空間,我們使用8個哈希函數映射,此時誤判率已經到了0.0005,也就是萬分之5,回到開頭的URL黑名單問題,我們上面假設一條URL有40字節也就是320比特,假如使用布隆過濾器來做黑名單問題,相當於只用了存儲每條URL情況的二十分之一甚至更低,帶來了萬分之五的錯誤率,而其復雜度也很低,因為只需要經過幾次簡單的哈希運算。
2.3 使用(以Java為例)
了解了原理后使用布隆過濾器你可以自行設計,也可以使用Google 開源的 Guava 中自帶的布隆過濾器,這里簡單介紹一下它的使用,首先需要引入依賴
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
創建一個布隆過濾器
BloomFilter<String> filter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000,
0.001);
其中第create函數的2、3個參數,可以顯式的控制數據量和誤判率,其就是通過本文2.3中講到的誤判率那樣操作。
添加數據和判斷是否存在
filter.put("bilibili.com");
filter.put("cnblogs.com");
System.out.println(filter.mightContain("bilibili.com"));
System.out.println(filter.mightContain("zhihu.com"));
上面設置的誤判率是0.001,所以當mightContain()
函數返回true時,我們可以99.9%的確認,判斷的數據存在於布隆過濾器中。它的缺陷就是不能進行刪除操作,而且只能單機使用。
另外分布式環境下,Redis中也可以使用布隆過濾器,Redis v4.0 之后有了 Module 功能,可以使用官方推薦的第三方布隆過濾器插件https://github.com/RedisBloom/RedisBloom。
2.4 實際應用
海量數據下,通過設計正確適用的布隆過濾器以很低的錯誤率帶來了幾十倍的內存開銷降低,其應用范圍也很廣,比如
- 識別惡意郵箱地址。
- URL黑名單、白名單,比如Chrome瀏覽器就是使用了一個布隆過濾器識別惡意鏈接。
- 解決緩存穿透問題,緩存穿透指查詢一個不存在的數據,這時候緩存中不存在,就會不斷的查詢數據庫,造成不必要的IO,而且有人如果惡意使用不存在的key也可以對數據庫進行攻擊。
- 果蠅....通過改進的布隆過濾器來檢測新鮮氣味。(混入一個奇怪的東西)
- 谷歌Bigtable、Apache HBase、Apache Cassandra和PostgreSQL使用布隆過濾器來減少對不存在的行或列的磁盤查找。
- ......