1. 程式人生 > >布隆過濾器的原理、使用場景和注意事項

布隆過濾器的原理、使用場景和注意事項

今天碰到個業務,他的 Redis 叢集有個大 Value 用途是作為布隆過濾器,但溝通的時候被小懟了一下,意思大概是 “布隆過濾器原理都不懂,還要我優化?”。技術菜被人懟認了、怪不得別人,自己之前確實只是聽說過這個,但是沒深入瞭解過,趁這個機會補充一下知識。
在進入正文之前,之前看到的有句話我覺得說得很好:

Data structures are nothing different. They are like the bookshelves of your application where you can organize your data. Different data structures will give you different facility and benefits. To properly use the power and accessibility of the data structures you need to know the trade-offs of using one.

大意是不同的資料結構有不同的適用場景和優缺點,你需要仔細權衡自己的需求之後妥善適用它們,布隆過濾器就是踐行這句話的代表。

什麼是布隆過濾器

本質上布隆過濾器是一種資料結構,比較巧妙的概率型資料結構(probabilistic data structure),特點是高效地插入和查詢,可以用來告訴你 “某樣東西一定不存在或者可能存在”。
相比於傳統的 List、Set、Map 等資料結構,它更高效、佔用空間更少,但是缺點是其返回的結果是概率性的,而不是確切的。

實現原理

HashMap 的問題

講述布隆過濾器的原理之前,我們先思考一下,通常你判斷某個元素是否存在用的是什麼?應該蠻多人回答 HashMap 吧,確實可以將值對映到 HashMap 的 Key,然後可以在 O(1) 的時間複雜度內返回結果,效率奇高。但是 HashMap 的實現也有缺點,例如儲存容量佔比高,考慮到負載因子的存在,通常空間是不能被用滿的,而一旦你的值很多例如上億的時候,那 HashMap 佔據的記憶體大小就變得很可觀了。
還比如說你的資料集儲存在遠端伺服器上,本地服務接受輸入,而資料集非常大不可能一次性讀進記憶體構建 HashMap 的時候,也會存在問題。

布隆過濾器資料結構

布隆過濾器是一個 bit 向量或者說 bit 陣列,長這樣:

在這裡插入圖片描述

如果我們要對映一個值到布隆過濾器中,我們需要使用多個不同的雜湊函式生成多個雜湊值,並對每個生成的雜湊值指向的 bit 位置 1,例如針對值 “baidu” 和三個不同的雜湊函式分別生成了雜湊值 1、4、7,則上圖轉變為:
在這裡插入圖片描述

Ok,我們現在再存一個值 “tencent”,如果雜湊函式返回 3、4、8 的話,圖繼續變為:
在這裡插入圖片描述

值得注意的是,4 這個 bit 位由於兩個值的雜湊函式都返回了這個 bit 位,因此它被覆蓋了。現在我們如果想查詢 “dianping” 這個值是否存在,雜湊函式返回了 1、5、8三個值,結果我們發現 5 這個 bit 位上的值為 0,說明沒有任何一個值對映到這個 bit 位上,因此我們可以很確定地說 “dianping” 這個值不存在。而當我們需要查詢 “baidu” 這個值是否存在的話,那麼雜湊函式必然會返回 1、4、7,然後我們檢查發現這三個 bit 位上的值均為 1,那麼我們可以說 “baidu” 存在了麼?答案是不可以,只能是 “baidu” 這個值可能存在。
這是為什麼呢?答案跟簡單,因為隨著增加的值越來越多,被置為 1 的 bit 位也會越來越多,這樣某個值 “taobao” 即使沒有被儲存過,但是萬一雜湊函式返回的三個 bit 位都被其他值置位了 1 ,那麼程式還是會判斷 “taobao” 這個值存在。

支援刪除麼

目前我們知道布隆過濾器可以支援 add 和 isExist 操作,那麼 delete 操作可以麼,答案是不可以,例如上圖中的 bit 位 4 被兩個值共同覆蓋的話,一旦你刪除其中一個值例如 “tencent” 而將其置位 0,那麼下次判斷另一個值例如 “baidu” 是否存在的話,會直接返回 false,而實際上你並沒有刪除它。
如何解決這個問題,答案是計數刪除。但是計數刪除需要儲存一個數值,而不是原先的 bit 位,會增大佔用的記憶體大小。這樣的話,增加一個值就是將對應索引槽上儲存的值加一,刪除則是減一,判斷是否存在則是看值是否大於0。

如何選擇雜湊函式個數和布隆過濾器長度

很顯然,過小的布隆過濾器很快所有的 bit 位均為 1,那麼查詢任何值都會返回“可能存在”,起不到過濾的目的了。布隆過濾器的長度會直接影響誤報率,布隆過濾器越長其誤報率越小。
另外,雜湊函式的個數也需要權衡,個數越多則布隆過濾器 bit 位置位 1 的速度越快,且布隆過濾器的效率越低;但是如果太少的話,那我們的誤報率會變高。

在這裡插入圖片描述
k 為雜湊函式個數,m 為布隆過濾器長度,n 為插入的元素個數,p 為誤報率。
至於如何推導這個公式,我在知乎釋出的文章有涉及,感興趣可以看看,不感興趣的話記住上面這個公式就行了。

最佳實踐

常見的適用常見有,利用布隆過濾器減少磁碟 IO 或者網路請求,因為一旦一個值必定不存在的話,我們可以不用進行後續昂貴的查詢請求。
另外,既然你使用布隆過濾器來加速查詢和判斷是否存在,那麼效能很低的雜湊函式不是個好選擇,推薦 MurmurHash、Fnv 這些。

大Value拆分

Redis 因其支援 setbit 和 getbit 操作,且純記憶體效能高等特點,因此天然就可以作為布隆過濾器來使用。但是布隆過濾器的不當使用極易產生大 Value,增加 Redis 阻塞風險,因此生成環境中建議對體積龐大的布隆過濾器進行拆分。
拆分的形式方法多種多樣,但是本質是不要將 Hash(Key) 之後的請求分散在多個節點的多個小 bitmap 上,而是應該拆分成多個小 bitmap 之後,對一個 Key 的所有雜湊函式都落在這一個小 bitmap 上。


轉載自:
作者:YoungChen__
連結:https://www.jianshu.com/p/2104d11ee0a2