布隆過濾器是由 Burton Bloom 在 1970 年提出,所以也稱爲 Bloom Filter。java
做用網頁爬蟲
判斷一個元素是否在一個集合數組
實現緩存
經過一個很長的二進制向量和一系列的隨機映射函數實現數據結構
優勢app
插入/查詢速度快,且有較好的時間和空間效率。時間/空間複雜度都是常量 O(K)dom
缺點ide
有誤算率:斷定不存在則必定不存在,斷定存在則可能實際不存在。隨着插入元素數量增長,誤算率也增大函數
不能進行刪除google
使用場景
防止緩存擊穿
Web 攔截器
網頁爬蟲對 URL 去重
反垃圾郵件,判斷某個郵箱是不是垃圾郵
......
1. 實現原理
當須要判斷一個元素是否在某個集合中時,咱們首先想到的是使用集合類(鏈表、樹、散列表等)來實現。可是使用集合類,咱們是直接將全部元素保存在集合中,隨着集合中元素數量的增長,集合所須要的存儲空間也會線性增加,同時查詢速度也會愈來愈慢。
使用數組或列表時,插入項的位置和要插入的值沒有對應關係,這樣當須要查找某個值時就必須遍歷已有的元素;當數組或列表中存在的元素數據不少時,就會影響查詢效率。
針對上述數組查詢問題,咱們改成使用哈希表。哈希表經過對「值」進行哈希計算,而後模哈希桶大小,獲得存放該值的列表索引位置。查詢時根據值的哈希值快速定位到存放該值的索引位置,而後在索引位置列表中進行查找該值是否存在,這樣縮小了查詢範圍,提升了查詢的效率。由於咱們是將全部值存放在哈希表中,因此當存放值的數量變多時,會佔用大量的存儲空間,從而影響查詢效率或者發生內存溢出。
這時,咱們可使用布隆過濾器(Bloom Filter)。不論是集合類仍是布隆過濾器,都用到了哈希函數(Hash Function),咱們先來看看哈希函數的定義。
哈希函數
哈希函數,也稱爲散列函數,給定一個輸入值 x,那麼通過哈希函數計算輸出 H(x),這個計算結果咱們稱爲哈希值或者散列值。哈希函數具備如下特徵:
輸入 x 能夠任意長度
輸出結果 H(x) 的長度固定
計算 H(x) 過程高效,長度爲 n 的 x,H(x) 的時間複雜度應爲 O(n)
單向散列:當兩個散列值不相同時,那麼計算它們的輸入值必定也不相同
散列碰撞:當兩個散列值相同,可是計算它們的輸入值不相同時,咱們稱這種狀況爲散列碰撞
隱匿性:經過 H(x) 結果,不能從計算上逆向推導出 x,不存在比窮舉法更好的方法
布隆過濾器數據結構
布隆過濾器(Bloom Filter)本質上是一個長度爲m 的位向量或位列表,僅包含二進制的 0 或 1。最初全部的值均爲 0。
爲了減小哈希碰撞,布隆過濾器使用 K 個不一樣的哈希函數,並將哈希結果對應的位置爲 1。
如上圖所示:當輸入 January 時,經過設置當 3 個哈希函數計算獲得索引位置 一、四、6,咱們將這 3 個索引位置置爲 1。
而後再輸入 February,根據哈希函數獲得索引位置 一、五、8。由於索引位 1 上已是 1,所以只須要將 五、8 索引位置 1。
當查詢某個值是否存在時,一樣經過設置的哈希函數計算出索引位置,若是相應索引位置上都是 1 則說明該值已存在(存在誤判的狀況),不然該值必定不存在。
布隆過濾器爲何不能刪除?如上所示,咱們若是刪除 February,則將索引位置 一、五、8 置 0。此時咱們再來判斷查詢 January 是否存在,由於索引位 1 已經置 0,根據判斷規則,則斷定 January 不存在。所以在布隆過濾器中刪除元素會致使其餘已存在元素的誤判。
2. Guava Bloom Filter 實現
Google 的 Guava 庫中提供了 Bloom Filter 的實現。咱們使用它來實現一個從 1000 萬數據中判斷隨機的 10000 條數據是否存在。
- 引入 guava 庫
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1-jre</version></dependency>
- 代碼示例
import com.google.common.base.Charsets;import com.google.common.hash.BloomFilter;import com.google.common.hash.Funnels;
import java.util.Random;
public class BloomFilterDemo {
public static void main(String[] args) { // 放入 1000 萬條數據 int total = 10000000; BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total); for (int i = 0; i < total; i++) { bloomFilter.put("" + i); }
// 隨機查找 10000 個數 long startTime = System.currentTimeMillis(); int count1 = 0; int count2 = 0; for (int i = 0; i < 10000; i++) { int num = new Random().nextInt(100000000); if (num < total) { count1++; } String numStr = num + ""; if (bloomFilter.mightContain(numStr)) { count2++; } } System.out.println("匹配數據:" + count2 + "條,耗時" + (System.currentTimeMillis() - startTime) + "ms"); if (count1 != count2) { System.out.println("誤判:" + Math.abs(count2 - count1) + "條"); } }
}
- 代碼輸出
匹配數據:1276條,耗時9ms誤判:286條
如上代碼所示,布隆過濾器存在誤判率,使用 Guava Bloom Filter 時,咱們能夠在建立布隆過濾器時設置誤判率 FPP 來提升匹配準確度。如將上述代碼中 BloomFilter 的建立語句改成:
BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total, 0.0001);
Guava BloomFilter 默認誤判率 fpp 爲`0.03D`,fpp 越小,匹配精度越高,相應的須要的存儲空間越大,因此在實際應用中根據實際業務狀況在誤判率和存儲空間之間選取一個合適的值。