在程序的世界中,布隆過濾器是程序員的一把利器,利用它能夠快速地解決項目中一些比較棘手的問題。如網頁 URL 去重、垃圾郵件識別、大集合中重複元素的判斷和緩存穿透等問題。html
布隆過濾器(Bloom Filter)是 1970 年由布隆提出的。它其實是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器能夠用於檢索一個元素是否在一個集合中。它的優勢是空間效率和查詢時間都比通常的算法要好的多,缺點是有必定的誤識別率和刪除困難。java
閱讀更多關於 Angular、TypeScript、Node.js/Java 、Spring 等技術文章,歡迎訪問個人我的博客 ——全棧修仙之路
當你往簡單數組或列表中插入新數據時,將不會根據插入項的值來肯定該插入項的索引值。這意味着新插入項的索引值與數據值之間沒有直接關係。這樣的話,當你須要在數組或列表中搜索相應值的時候,你必須遍歷已有的集合。若集合中存在大量的數據,就會影響數據查找的效率。linux
針對這個問題,你能夠考慮使用哈希表。利用哈希表你能夠經過對 「值」 進行哈希處理來得到該值對應的鍵或索引值,而後把該值存放到列表中對應的索引位置。這意味着索引值是由插入項的值所肯定的,當你須要判斷列表中是否存在該值時,只須要對值進行哈希處理並在相應的索引位置進行搜索便可,這時的搜索速度是很是快的。程序員
根據定義,布隆過濾器能夠檢查值是 「可能在集合中」 仍是 「絕對不在集合中」。「可能」 表示有必定的機率,也就是說可能存在必定爲誤判率。那爲何會存在誤判呢?下面咱們來分析一下具體的緣由。算法
布隆過濾器(Bloom Filter)本質上是由長度爲 m 的位向量或位列表(僅包含 0 或 1 位值的列表)組成,最初全部的值均設置爲 0,以下圖所示。數據庫
爲了將數據項添加到布隆過濾器中,咱們會提供 K 個不一樣的哈希函數,並將結果位置上對應位的值置爲 「1」。在前面所提到的哈希表中,咱們使用的是單個哈希函數,所以只能輸出單個索引值。而對於布隆過濾器來講,咱們將使用多個哈希函數,這將會產生多個索引值。網頁爬蟲
如上圖所示,當輸入 「semlinker」 時,預設的 3 個哈希函數將輸出 二、四、6,咱們把相應位置 1。假設另外一個輸入 」kakuqo「,哈希函數輸出 三、4 和 7。你可能已經注意到,索引位 4 已經被先前的 「semlinker」 標記了。此時,咱們已經使用 「semlinker」 和 」kakuqo「 兩個輸入值,填充了位向量。當前位向量的標記狀態爲:segmentfault
當對值進行搜索時,與哈希表相似,咱們將使用 3 個哈希函數對 」搜索的值「 進行哈希運算,並查看其生成的索引值。假設,當咱們搜索 」fullstack「 時,3 個哈希函數輸出的 3 個索引值分別是 二、3 和 7:數組
從上圖能夠看出,相應的索引位都被置爲 1,這意味着咱們能夠說 」fullstack「 可能已經插入到集合中。事實上這是誤報的情形,產生的緣由是因爲哈希碰撞致使的巧合而將不一樣的元素存儲在相同的比特位上。幸運的是,布隆過濾器有一個可預測的誤判率(FPP):緩存
n
是已經添加元素的數量;k
哈希的次數;m
布隆過濾器的長度(如比特數組的大小)。極端狀況下,當布隆過濾器沒有空閒空間時(滿),每一次查詢都會返回 true
。這也就意味着 m
的選擇取決於指望預計添加元素的數量 n
,而且 m
須要遠遠大於 n
。
實際狀況中,布隆過濾器的長度 m
能夠根據給定的誤判率(FFP)的和指望添加的元素個數 n
的經過以下公式計算:
對於 m/n 比率表示每個元素須要分配的比特位的數量,也就是哈希函數 k
的數量能夠調整誤判率。經過以下公式來選擇最佳的 k
能夠減小誤判率(FPP):
瞭解完上述的內容以後,咱們能夠得出一個結論,當咱們搜索一個值的時候,若該值通過 K 個哈希函數運算後的任何一個索引位爲 」0「,那麼該值確定不在集合中。但若是全部哈希索引值均爲 」1「,則只能說該搜索的值可能存在集合中。
在實際工做中,布隆過濾器常見的應用場景以下:
除了上述的應用場景以外,布隆過濾器還有一個應用場景就是解決緩存穿透的問題。所謂的緩存穿透就是服務調用方每次都是查詢不在緩存中的數據,這樣每次服務調用都會到數據庫中進行查詢,若是這類請求比較多的話,就會致使數據庫壓力增大,這樣緩存就失去了意義。
利用布隆過濾器咱們能夠預先把數據查詢的主鍵,好比用戶 ID 或文章 ID 緩存到過濾器中。當根據 ID 進行數據查詢的時候,咱們先判斷該 ID 是否存在,若存在的話,則進行下一步處理。若不存在的話,直接返回,這樣就不會觸發後續的數據庫查詢。須要注意的是緩存穿透不能徹底解決,咱們只能將其控制在一個能夠容忍的範圍內。
布隆過濾器有不少實現和優化,由 Google 開發著名的 Guava 庫就提供了布隆過濾器(Bloom Filter)的實現。在基於 Maven 的 Java 項目中要使用 Guava 提供的布隆過濾器,只須要引入如下座標:
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.0-jre</version> </dependency>
在導入 Guava 庫後,咱們新建一個 BloomFilterDemo 類,在 main 方法中咱們經過 BloomFilter.create 方法來建立一個布隆過濾器,接着咱們初始化 1 百萬條數據到過濾器中,而後在原有的基礎上增長 10000 條數據並判斷這些數據是否存在布隆過濾器中:
import com.google.common.base.Charsets; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; public class BloomFilterDemo { public static void main(String[] args) { int total = 1000000; // 總數量 BloomFilter<CharSequence> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total); // 初始化 1000000 條數據到過濾器中 for (int i = 0; i < total; i++) { bf.put("" + i); } // 判斷值是否存在過濾器中 int count = 0; for (int i = 0; i < total + 10000; i++) { if (bf.mightContain("" + i)) { count++; } } System.out.println("已匹配數量 " + count); } }
當以上代碼運行後,控制檯會輸出如下結果:
已匹配數量 1000309
很明顯以上的輸出結果已經出現了誤報,由於相比預期的結果多了 309 個元素,誤判率爲:
309/(1000000 + 10000) * 100 ≈ 0.030594059405940593
若是要提升匹配精度的話,咱們能夠在建立布隆過濾器的時候設置誤判率 fpp:
BloomFilter<CharSequence> bf = BloomFilter.create( Funnels.stringFunnel(Charsets.UTF_8), total, 0.0002 );
在 BloomFilter 內部,誤判率 fpp 的默認值是 0.03:
// com/google/common/hash/BloomFilter.class public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) { return create(funnel, expectedInsertions, 0.03D); }
在從新設置誤判率爲 0.0002 以後,咱們從新運行程序,這時控制檯會輸出如下結果:
已匹配數量 1000003
經過觀察以上的結果,可知誤判率 fpp 的值越小,匹配的精度越高。當減小誤判率 fpp 的值,須要的存儲空間也越大,因此在實際使用過程當中須要在誤判率和存儲空間之間作個權衡。
爲了便於你們理解布隆過濾器,咱們來看一下下面簡易版布隆過濾器。
package com.semlinker.bloomfilter; import java.util.BitSet; public class SimpleBloomFilter { private static final int DEFAULT_SIZE = 2 << 24; private static final int[] seeds = new int[]{7, 11, 13, 31, 37, 61}; private BitSet bits = new BitSet(DEFAULT_SIZE); private SimpleHash[] func = new SimpleHash[seeds.length]; public SimpleBloomFilter() { // 建立多個哈希函數 for (int i = 0; i < seeds.length; i++) { func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]); } } /** * 添加元素到布隆過濾器中 * * @param value */ public void put(String value) { for (SimpleHash f : func) { bits.set(f.hash(value), true); } } /** * 判斷布隆過濾器中是否包含指定元素 * * @param value * @return */ public boolean mightContain(String value) { if (value == null) { return false; } boolean ret = true; for (SimpleHash f : func) { ret = ret && bits.get(f.hash(value)); } return ret; } public static void main(String[] args) { SimpleBloomFilter bf = new SimpleBloomFilter(); for (int i = 0; i < 1000000; i++) { bf.put("" + i); } // 判斷值是否存在過濾器中 int count = 0; for (int i = 0; i < 1000000 + 10000; i++) { if (bf.mightContain("" + i)) { count++; } } System.out.println("已匹配數量 " + count); } /** * 簡單哈希類 */ public static class SimpleHash { private int cap; private int seed; public SimpleHash(int cap, int seed) { this.cap = cap; this.seed = seed; } public int hash(String value) { int result = 0; int len = value.length(); for (int i = 0; i < len; i++) { result = seed * result + value.charAt(i); } return (cap - 1) & result; } } }
在 SimpleBloomFilter 類的實現中,咱們使用到了 Java util 包中的 BitSet,BitSet 是位操做的對象,值只有 0 或 1 ,內部維護了一個 long 數組,初始只有一個 long,因此 BitSet 最小的容量是 64 位。當隨着存儲的元素愈來愈多,BitSet 內部會動態擴容,最終內部是由 N 個 long 值來存儲。默認狀況下,BitSet 的全部位都是 0。
本文主要介紹的布隆過濾器的概念和常見的應用場合,在實戰部分咱們演示了 Google 著名的 Guava 庫所提供布隆過濾器(Bloom Filter)的基本使用,同時咱們也介紹了布隆過濾器出現誤報的緣由及如何提升判斷準確性。最後爲了便於你們理解布隆過濾器,咱們介紹了一個簡易版的布隆過濾器 SimpleBloomFilter。