1. 程式人生 > 其它 >布隆過濾器(Bloom Filter)詳解及應用

布隆過濾器(Bloom Filter)詳解及應用

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,這樣來一條查詢需求,判斷bit陣列中多個位置是否都為true即可,相比點陣圖它在有限的空間內,儘可能的降低了查詢失敗的可能。

以上圖為例,假設對bilibli.com進行兩次hash運算

\[Hash_1(bilibli.com) \% 12 = 4 \]\[Hash_2(bilibli.com) \% 12 = 6 \]

得到結果後,令BitSet陣列中下標為4和6的位置1,同樣對cnblogs.com進行兩次hash計算對映到陣列中下標0和4的位置,置1,那麼假如來一條查詢資訊,只需要同樣計算兩次雜湊,若同時為1,則返回true即可。

而實際操作中,雜湊函式一般會選取多個,比如常用的8個雜湊函式,儘可能的在有限的空間內降低查詢出現雜湊衝突的可能,但是衝突現象顯然是無法避免的,智慧根據需求,通過合理的選擇位陣列的大小,來儘可能的將衝突降低。

另外布隆過濾器存在的一個問題就是,無法對黑名單進行刪除操作,比如上述的例子,下標為4的位置是兩條資料經過不同的雜湊運算後得到了同樣的結果,加入要刪除一條資料必然影響其他資料的查詢結果,造成更高的誤判率。

2.2 誤判率

布隆過濾器的誤判率也可以稱之為假陽性(false positive)的概率,比如來一條URL查詢是否在黑名單中,結果其對應的雜湊結果已經被其他一個或者多個URL置1,那麼此時就出現了查詢錯誤的情況。所以布隆過濾器只適合有記憶體開銷限制、並且允許出現錯誤率的情況,我們可以通過分析其出現錯誤的概率,選取合適的bit陣列大小以及雜湊函式的個數,儘可能在記憶體開銷和錯誤率中間進行一個折中的選擇。

假設bit陣列大小為m,資料量為n,使用k個不同的雜湊函式對映,那麼考慮隨機雜湊情況下,陣列中某一個位置在一次雜湊後被置1的概率為

\[\frac1m \]

那麼在一次雜湊過後,陣列中某一位為0的概率即為

\[1-\frac1m \]

經過k個雜湊函式後,某一個位置為0的概率即為

\[(1-\frac1m)^k \]

考慮m足夠大的情況下有

\[\lim\limits_{m\rightarrow\infty}(1-\frac1m)^m=\frac1e \]

所以有k個雜湊函式後,某一個位置為0的概率即為

\[(1-\frac1m)^k\approx e^{\frac{-k}{m}} \]

則插入n個數後,某個位置仍然為0的概率為

\[(1-\frac1m)^{kn}\approx e^{\frac{-kn}{m}} \]

所以插入n個數後,bit陣列中某個位置為1的概率為

\[1-e^{\frac{-kn}{m}} \]

所以在插入n個數後,來一條查詢資料,資料經過k個雜湊函式對映後,bit陣列中k個位置均為1的概率為:

\[P\approx (1-e^{\frac{-kn}{m}})^k \]

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位元組也就是160位元,假如使用布隆過濾器來做黑名單問題,相當於只用了儲存每條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);

其中第creat函式的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使用布隆過濾器來減少對不存在的行或列的磁碟查詢
  • ......

3 布穀鳥過濾器

布隆過濾器在工程應用方面已經比較成熟了,上面談到了布隆過濾器的一些缺點,比如不支援刪除操作、查詢效率弱,因為多個隨機雜湊函式探測的是bit陣列中多個不同的點,所以會導致低CPU快取命中率。

針對此2014年的一篇文章《Cuckoo Filter:Better Than Bloom》提出了布穀鳥過濾器,不過看文章的名字有點碰瓷的感覺了,這篇文章解決了布隆過濾器存在的問題。但是搜了下還沒有很具體的工程應用。