今天碰到個業務,他的 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),特色是高效地插入和查詢,能夠用來告訴你 「某樣東西必定不存在或者可能存在」。app
相比於傳統的 List、Set、Map 等數據結構,它更高效、佔用空間更少,可是缺點是其返回的結果是機率性的,而不是確切的。函數
講述布隆過濾器的原理以前,咱們先思考一下,一般你判斷某個元素是否存在用的是什麼?應該蠻多人回答 HashMap 吧,確實能夠將值映射到 HashMap 的 Key,而後能夠在 O(1) 的時間複雜度內返回結果,效率奇高。可是 HashMap 的實現也有缺點,例如存儲容量佔比高,考慮到負載因子的存在,一般空間是不能被用滿的,而一旦你的值不少例如上億的時候,那 HashMap 佔據的內存大小就變得很可觀了。性能
還好比說你的數據集存儲在遠程服務器上,本地服務接受輸入,而數據集很是大不可能一次性讀進內存構建 HashMap 的時候,也會存在問題。優化
布隆過濾器是一個 bit 向量或者說 bit 數組,長這樣:orm
若是咱們要映射一個值到布隆過濾器中,咱們須要使用多個不一樣的哈希函數生成多個哈希值,並對每一個生成的哈希值指向的 bit 位置 1,例如針對值 「baidu」 和三個不一樣的哈希函數分別生成了哈希值 一、四、7,則上圖轉變爲:索引
Ok,咱們如今再存一個值 「tencent」,若是哈希函數返回 三、四、8 的話,圖繼續變爲:
值得注意的是,4 這個 bit 位因爲兩個值的哈希函數都返回了這個 bit 位,所以它被覆蓋了。如今咱們若是想查詢 「dianping」 這個值是否存在,哈希函數返回了 一、五、8三個值,結果咱們發現 5 這個 bit 位上的值爲 0,說明沒有任何一個值映射到這個 bit 位上,所以咱們能夠很肯定地說 「dianping」 這個值不存在。而當咱們須要查詢 「baidu」 這個值是否存在的話,那麼哈希函數必然會返回 一、四、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 這些。
Redis 因其支持 setbit 和 getbit 操做,且純內存性能高等特色,所以自然就能夠做爲布隆過濾器來使用。可是布隆過濾器的不當使用極易產生大 Value,增長 Redis 阻塞風險,所以生成環境中建議對體積龐大的布隆過濾器進行拆分。
拆分的形式方法多種多樣,可是本質是不要將 Hash(Key) 以後的請求分散在多個節點的多個小 bitmap 上,而是應該拆分紅多個小 bitmap 以後,對一個 Key 的全部哈希函數都落在這一個小 bitmap 上。