介紹:
布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都比一般的算法要好的多,缺點是有一定的誤識別率和刪除困難。
應用例子:
- 網頁URL的去重;
- 垃圾郵件的判別;
- 集合重復元素的判別;
- 查詢加速(比如基於key-value的存儲系統)等。
前提知識點:Bitset
Java BitSet可以按位存儲,計算機中一個字節(byte)占8位(bit);
而BitSet是位操作的對象,值只有0或1(即true 和 false),內部維護一個long數組,初始化只有一個long segement,所以BitSet最小的size是64;隨着存儲的元素越來越多,BitSet內部會自動擴充,一次擴充64位,最終內部是由N個long segement 來存儲;
默認情況下,BitSet所有位都是0即false;基本原理是,用1位來表示一個數據是否出現過,0為沒有出現過,1表示出現過。使用用的時候既可根據某一個是否為0表示此數是否出現過。
一個1G的空間,有 8*1024*1024*1024=8.58*10^9bit,也就是可以表示85億個不同的數。
例子:
比如有一堆數字,需要存儲,source=[3,5,6,9]
用int就需要4*4個字節。
java.util.BitSet可以存true/false。
如果用java.util.BitSet,則會少很多,其原理是:
1,先找出數據中最大值maxvalue=9
2,聲明一個BitSet bs,它的size是maxvalue+1=10
3,遍歷數據source,bs[source[i]]設置成true.
最后的值是: (0為false;1為true)
bs [ 0, 0, 0, 1, 0, 1, 1, 0, 0, 1]
3, 5, 6, 9
這樣一個本來要int型需要占4字節共32位的數字現在只用了1位!
比例32:1
這樣就省下了很大空間
通常用在數據統計、分析的領域
jdk實現BitSet底層為什么使用long,不用int?
JDK選擇long數組作為BitSet的內部存儲結構是出於性能的考慮,因為BitSet提供and和or這種操作,需要對兩個BitSet中的所有bit位做and或者or,實現的時候需要遍歷所有的數組元素。使用long能夠使得循環的次數降到最低,所以Java選擇使用long數組作為BitSet的內部存儲結構。
從數據在棧上的存儲來說,使用long和byte基本是沒有什么差別的,除了編譯器強制地址對齊的時候,使用byte最多會浪費7個字節(強制按照8的倍數做地址對其),另外從內存讀數組元素的時候,也是沒有什么區別的,因為匯編指令有對不同長度數據的mov指令。所以說,JDK選擇使用long數組作為BitSet的內部存儲結構的根本原因就是在and和or的時候減少循環次數,提高性能。
布龍過濾器的原理
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” 這個值存在。
如何選擇哈希函數個數和布隆過濾器長度
很顯然,過小的布隆過濾器很快所有的 bit 位均為 1,那么查詢任何值都會返回“可能存在”,起不到過濾的目的了。布隆過濾器的長度會直接影響誤報率,布隆過濾器越長其誤報率越小。
另外,哈希函數的個數也需要權衡,個數越多則布隆過濾器 bit 位置位 1 的速度越快,且布隆過濾器的效率越低;但是如果太少的話,那我們的誤報率會變高。

k 為哈希函數個數,m 為布隆過濾器長度,n 為插入的元素個數,p 為誤報率。
如何選擇適合業務的 k 和 m 值呢,這里直接貼一個公式:
如何推導這個公式這里只是提一句,因為對於使用來說並沒有太大的意義,你讓一個高中生來推會推得很快。k 次哈希函數某一 bit 位未被置為 1 的概率為:
插入n個元素后依舊為 0 的概率和為 1 的概率分別是:
標明某個元素是否在集合中所需的 k 個位置都按照如上的方法設置為 1,但是該方法可能會使算法錯誤的認為某一原本不在集合中的元素卻被檢測為在該集合中(False Positives),該概率由以下公式確定
最佳實踐
常見的適用常見有,利用布隆過濾器減少磁盤 IO 或者網絡請求,因為一旦一個值必定不存在的話,我們可以不用進行后續昂貴的查詢請求。
另外,既然你使用布隆過濾器來加速查找和判斷是否存在,那么性能很低的哈希函數不是個好選擇,推薦 MurmurHash、Fnv 這些。
大Value拆分
Redis 因其支持 setbit 和 getbit 操作,且純內存性能高等特點,因此天然就可以作為布隆過濾器來使用。但是布隆過濾器的不當使用極易產生大 Value,增加 Redis 阻塞風險,因此生成環境中建議對體積龐大的布隆過濾器進行拆分。
拆分的形式方法多種多樣,但是本質是不要將 Hash(Key) 之后的請求分散在多個節點的多個小 bitmap 上,而是應該拆分成多個小 bitmap 之后,對一個 Key 的所有哈希函數都落在這一個小 bitmap 上。
資料:https://zhuanlan.zhihu.com/p/43263751