布隆過濾器(BloomFilter)原理 實現和性能測試

布隆過濾器(BloomFilter)是一種你們在學校沒怎麼學過,但在計算機不少領域很是經常使用的數據結構,它能夠用來高效判斷某個key是否屬於一個集合,有極高的插入和查詢效率(O(1)),也很是省存儲空間。固然它也不是天衣無縫,它也有本身的缺點,接下來跟隨我一塊兒詳細瞭解下BloomFilter的實現原理,以及它優缺點、應用場景,最後再看下Google guava包中BloomFilter的實現,並對比下它和HashSet在不一樣數據量下內存空間的使用狀況。 java

學過數據結構的人都知道,在計算機領域咱們常常經過犧牲空間換時間,或者犧牲時間換空間,BloomFilter給了咱們一種新的思路——犧牲準確率換空間。是的,BloomFilter不是100%準確的,它是有可能有誤判,但絕對不會有漏判斷,說通俗點就是,BloomFilter有可能錯殺好人,但不會放過任何一個壞人。BloomFilter最大的優勢就是省空間,缺點就是否是100%準確,這點固然和它的實現原理有關。編程

BloomFilter的原理

在講解BloomFilter的實現前,咱們先來了解下什麼叫Bitmap(位圖),先給你一道《編程珠璣》上的題目。數組

給你一個有100w個數的集合S,每一個數的數據大小都是0-100w,有些數據重複出現,這就意味着有些數據可能都沒出現過,讓你以O(n)的時間複雜度找出0-100w之間沒有出如今S中的數,儘量減小內存的使用。

既然時間複雜度都限制是O(N)了,意味着咱們不能使用排序了,咱們能夠開一個長爲100w的int數組來標記下哪些數字出現過,在就標1,不在標0。但對於每一個數來講咱們只須要知道它在不在,只是0和1的區別,用int(32位)有點太浪費空間了,咱們能夠按二進制位來用每一個int,這樣一個int就能夠標記32個數,空間佔用率一會兒減小到原來的1/32,因而咱們就有了位圖,Bitmap就是用n個二進制位來記錄0-m之間的某個數有沒有出現過。 瀏覽器

Bitmap的侷限在於它的存儲數據類型有限,只能存0-m之間的數,其餘的數據就存不了了。若是咱們想存字符串或者其餘數據怎麼辦?其實也簡單,只須要實現一個hash函數,將你要存的數據映射到0-m之間就好了。這裏假設你的hash函數產生的映射值是均勻的,咱們來計算下一個m位的Bitmap到底能存多少數據?
當你在Bitmap中插入了一個數後,經過hash函數計算它在Bitmap中的位置並將其置爲1,這時任意一個位置沒有被標爲1的機率是:數據結構

$$ 1 - \frac{1}{m} $$app

當插入n個數後,這個機率會變成:函數

$$ (1 - \frac{1}{m})^n $$性能

因此任意一個位置被標記成1的機率就是:測試

$$ P_1 = 1 - (1 - \frac{1}{m})^n $$大數據

這時候你判斷某個key是否在這個集合S中時,只須要看下這個key在hash在Bitmap上對應的位置是否爲1就好了,由於兩個key對應的hash值多是同樣的,因此有可能會誤判,你以前插入了a,可是hash(b)==hash(a),這時候你判斷b是否在集合S中時,看到的是a的結果,實際上b沒有插入過。

從上面公式中能夠看出有 $P_1$ 的機率可能會誤判,尤爲當n比較大時這個誤判機率仍是挺大的。 如何減小這個誤判率?咱們最開始是隻取了一個hash函數,若是說取k個不一樣的hash函數呢!咱們每插入一個數據,計算k個hash值,並對k位置爲1。在查找時,也是求k個hash值,而後看其是否都爲1,若是都爲1確定就能夠認爲這個數是在集合S中的。
問題來了,用k個hash函數,每次插入均可能會寫k位,更耗空間,那在一樣的m下,誤判率是否會更高呢?咱們來推導下。

在k個hash函數的狀況下,插入一個數後任意一個位置依舊是0的機率是:

$$ (1 - \frac{1}{m})^k $$

插入n個數後任意一個位置依舊是0的機率是:

$$ (1 - \frac{1}{m})^{kn} $$

因此可知,插入n個數後任意一個位置是1的機率是

$$ 1 - (1 - \frac{1}{m})^{kn} $$

由於咱們用是用k個hash共同來判斷是不是在集合中的,可知當用k個hash函數時其誤判率以下。它必定是比上面1個hash函數時誤判率要小(雖然我不會證實)

$$ \left(1-\left[1-\frac{1}{m}\right]^{k n}\right)^{k} < (1 - \left[1 - \frac{1}{m}\right]^n) $$

維基百科也給出了這個誤判率的近似公式(雖然我不知道是怎麼來的,因此這裏就直接引用了)

$$ \left(1-\left[1-\frac{1}{m}\right]^{k n}\right)^{k} \approx\left(1-e^{-k n / m}\right)^{k} $$

到這裏,咱們從新發明了Bloomfilter,就是這麼簡單,說白了Bloomfilter就是在Bitmap之上的擴展而已。對於一個key,用k個hash函數映射到Bitmap上,查找時只須要對要查找的內容一樣作k次hash映射,經過查看Bitmap上這k個位置是否都被標記了來判斷是否以前被插入過,以下圖。
在這裏插入圖片描述

經過公式推導和了解原理後,咱們已經知道Bloomfilter有個很大的缺點就是否是100%準確,有誤判的可能性。可是經過選取合適的bitmap大小和hash函數個數後,咱們能夠把誤判率降到很低,在大數據盛行的時代,適當犧牲準確率來減小存儲消耗仍是很值得的。

除了誤判以外,BloomFilter還有另一個很大的缺點 __只支持插入,沒法作刪除__。若是你想在Bloomfilter中刪除某個key,你不能直接將其對應的k個位所有置爲0,由於這些位置有多是被其餘key共享的。基於這個缺點也有一些支持刪除的BloomFilter的變種,適當犧牲了空間效率,感興趣能夠自行搜索下。

如何肯定最優的m和k?

知道原理後再來了解下怎麼去實現,咱們在決定使用Bloomfilter以前,須要知道兩個數據,一個是要存儲的數量n和預期的誤判率p。bitmap的大小m決定了存儲空間的大小,hash函數個數k決定了計算量的大小,咱們固然都但願m和k都越小越好,如何計算兩者的最優值,咱們大概來推導下。(備註:推導過程來自Wikipedia)

由上文可知,誤判率p爲

$$ p \approx \left(1-e^{-k n / m}\right)^{k} \ (1) $$

對於給定的m和n咱們想讓誤判率p最小,就得讓

$$ k=\frac{m}{n} \ln2 \ (2) $$

把(2)式代入(1)中可得

$$ p=\left(1-e^{-\left(\frac{m}{n} \ln 2\right) \frac{n}{m}}\right)^{\frac{m}{n} \ln 2} \ (3) $$

對(3)兩邊同時取ln並簡化後,獲得

$$ \ln p=-\frac{m}{n}(\ln 2)^{2} $$

最後能夠計算出m的最優值爲

$$ m=-\frac{n \ln p}{(\ln 2)^{2}} $$

由於誤判率p和要插入的數據量n是已知的,因此咱們能夠直接根據上式計算出m的值,而後把m和n的值代回到(2)式中就能夠獲得k的值。至此咱們就知道了實現一個bloomfilter所須要的全部參數了,接下來讓咱們看下Google guava包中是如何實現BloomFilter的。

guava中的BloomFilter

BloomFilter<T>沒法經過new去建立新對象,而它提供了create靜態方法來生成對象,其核心方法以下。

static <T> BloomFilter<T> create(
      Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
    checkNotNull(funnel);
    checkArgument(
        expectedInsertions >= 0, "Expected insertions (%s) must be >= 0", expectedInsertions);
    checkArgument(fpp > 0.0, "False positive probability (%s) must be > 0.0", fpp);
    checkArgument(fpp < 1.0, "False positive probability (%s) must be < 1.0", fpp);
    checkNotNull(strategy);

    if (expectedInsertions == 0) {
      expectedInsertions = 1;
    }

    long numBits = optimalNumOfBits(expectedInsertions, fpp);
    int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
    try {
      return new BloomFilter<T>(new LockFreeBitArray(numBits), numHashFunctions, funnel, strategy);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e);
    }
  }

從代碼能夠看出,須要4個參數,分別是

  • funnel 用來對參數作轉化,方便生成hash值
  • expectedInsertions 預期插入的數據量大小,也就是上文公式中的n
  • fpp 誤判率,也就是上文公式中的誤判率p
  • strategy 生成hash值的策略,guava中也提供了默認策略,通常不須要你本身從新實現

從上面代碼可知,BloomFilter建立過程當中先檢查參數的合法性,以後使用n和p來計算bitmap的大小m(optimalNumOfBits(expectedInsertions, fpp)),經過n和m計算hash函數的個數k(optimalNumOfHashFunctions(expectedInsertions, numBits)),這倆方法的具體實現以下。

static int optimalNumOfHashFunctions(long n, long m) {
    // (m / n) * log(2), but avoid truncation due to division!
    return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
  }
  static long optimalNumOfBits(long n, double p) {
    if (p == 0) {
      p = Double.MIN_VALUE;
    }
    return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
  }

其實就是上文中列出的計算公式。
後面插入和查找的邏輯就比較簡單了,這裏再也不贅述,有興趣能夠看下源碼,咱們這裏經過BloomFilter提供的方法列表瞭解下它的功能就行。
在這裏插入圖片描述
從上圖能夠看出,BloomFilter除了提供建立和幾個核心的功能外,還支持寫入Stream或從Stream中從新生成BloomFilter,方便數據的共享和傳輸。

使用案例

  • HBase、BigTable、Cassandra、PostgreSQ等著名開源項目都用BloomFilter來減小對磁盤的訪問次數,提高性能。
  • Chrome瀏覽器用BloomFilter來判別惡意網站。
  • 爬蟲用BloomFilter來判斷某個url是否爬取過。
  • 比特幣也用到了BloomFilter來加速錢包信息的同步。

……

和HashSet對比

咱們一直在說BloomFilter有巨大的存儲優點,作個優點到底有多明顯,咱們拿jdk自帶的HashSet和guava中實現的BloomFilter作下對比,數據僅供參考。

測試環境

測試平臺 Mac
guava(28.1)BloomFilter,JDK11(64位) HashSet
使用om.carrotsearch.java-sizeof計算實際佔用的內存空間

測試方式

BloomFilter vs HashSet

分別往BloomFilter和HashSet中插入UUID,總計插入100w個UUID,BloomFilter誤判率爲默認值0.03。每插入5w個統計下各自的佔用空間。結果以下,橫軸是數據量大小,縱軸是存儲空間,單位kb。
在這裏插入圖片描述
能夠看到BloomFilter存儲空間一直都沒有變,這裏和它的實現有關,事實上你在告訴它總共要插入多少條數據時BloomFilter就計算並申請好了內存空間,因此BloomFilter佔用內存不會隨插入數據的多少而變化。相反,HashSet在插入數據愈來愈多時,其佔用的內存空間也會愈來愈多,最終在插入完100w條數據後,其內存佔用爲BloomFilter的100多倍。

在不一樣fpp下的存儲表現

在不一樣的誤判率下,插入100w個UUID,計算其內存空間佔用。結果以下,橫軸是誤判率大小,縱軸是存儲空間,單位kb。
在這裏插入圖片描述

fpp,size
0.1,585.453125
0.01,1170.4765625
1.0E-3,1755.5
1.0E-4,2340.53125
1.0E-5,2925.5546875
1.0E-6,3510.578125
1.0E-7,4095.6015625
1.0E-8,4680.6328125
1.0E-9,5265.65625
1.0E-10,5850.6796875

能夠看出,在同等數據量的狀況下,BloomFilter的存儲空間和ln(fpp)呈反比,因此增加速率其實不算快,即使誤判率減小9個量級,其存儲空間也只是增長了10倍。

參考資料

  1. wikipedia bloom filter
相關文章
相關標籤/搜索