布隆過濾器詳解,全網最全一篇


前言

我們之前講了Redis的緩存雪崩、穿透、擊穿。在文章里我們說了解決緩存穿透的辦法之一,就是布隆過濾器,但是上次並沒有講如何使用布隆過濾器。

作為暖男的老哥,給你們補上,請叫我IT老暖男

什么是布隆過濾器

布隆過濾器(Bloom Filter),是1970年,由一個叫布隆的小伙子提出的,距今已經五十年了,和老哥一樣老。

它實際上是一個很長的二進制向量和一系列隨機映射函數,二進制大家應該都清楚,存儲的數據不是0就是1,默認是0。

主要用於判斷一個元素是否在一個集合中,0代表不存在某個數據,1代表存在某個數據。

懂了嗎?作為暖男的老哥在給你們畫張圖來幫助理解:

布隆過濾器用途

  • 解決Redis緩存穿透(今天重點講解)

  • 在爬蟲時,對爬蟲網址進行過濾,已經存在布隆中的網址,不在爬取。

  • 垃圾郵件過濾,對每一個發送郵件的地址進行判斷是否在布隆的黑名單中,如果在就判斷為垃圾郵件。

以上只是簡單的用途舉例,大家可以舉一反三,靈活運用在工作中。

布隆過濾器原理

存入過程

布隆過濾器上面說了,就是一個二進制數據的集合。當一個數據加入這個集合時,經歷如下洗禮(這里有缺點,下面會講):

  • 通過K個哈希函數計算該數據,返回K個計算出的hash值

  • 這些K個hash值映射到對應的K個二進制的數組下標

  • 將K個下標對應的二進制數據改成1。

例如,第一個哈希函數返回x,第二個第三個哈希函數返回y與z,那么: X、Y、Z對應的二進制改成1。

如圖所示:

查詢過程

布隆過濾器主要作用就是查詢一個數據,在不在這個二進制的集合中,查詢過程如下:

  • 通過K個哈希函數計算該數據,對應計算出的K個hash值

  • 通過hash值找到對應的二進制的數組下標

  • 判斷:如果存在一處位置的二進制數據是0,那么該數據不存在。如果都是1,該數據存在集合中。(這里有缺點,下面會講)

刪除過程

一般不能刪除布隆過濾器里的數據,這是一個缺點之一,我們下面會分析。

布隆過濾器的優缺點

優點

  • 由於存儲的是二進制數據,所以占用的空間很小

  • 它的插入和查詢速度是非常快的,時間復雜度是O(K),可以聯想一下HashMap的過程

  • 保密性很好,因為本身不存儲任何原始數據,只有二進制數據

缺點

這就要回到我們上面所說的那些缺點了。

添加數據是通過計算數據的hash值,那么很有可能存在這種情況:兩個不同的數據計算得到相同的hash值。

例如圖中的“你好”和“hello”,假如最終算出hash值相同,那么他們會將同一個下標的二進制數據改為1。

這個時候,你就不知道下標為2的二進制,到底是代表“你好”還是“hello”。

由此得出如下缺點:

一、存在誤判

假如上面的圖沒有存"hello",只存了"你好",那么用"hello"來查詢的時候,會判斷"hello"存在集合中。

因為“你好”和“hello”的hash值是相同的,通過相同的hash值,找到的二進制數據也是一樣的,都是1。

二、刪除困難

到這里我不說大家應該也明白為什么吧,作為你們的暖男老哥,還是講一下吧。

還是用上面的舉例,因為“你好”和“hello”的hash值相同,對應的數組下標也是一樣的。

這時候老哥想去刪除“你好”,將下標為2里的二進制數據,由1改成了0。

那么我們是不是連“hello”都一起刪了呀。(0代表有這個數據,1代表沒有這個數據)

到這里是不是對布隆過濾器已經明白了,都說了我是暖男。

實現布隆過濾器

有很多種實現方式,其中一種就是Guava提供的實現方式。

一、引入Guava pom配置

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>29.0-jre</version>
</dependency>
復制代碼

二、代碼實現

這里我們順便測一下它的誤判率。

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterCase {

  /**
   * 預計要插入多少數據
   */
  private static int size = 1000000;

  /**
   * 期望的誤判率
   */
  private static double fpp = 0.01;

  /**
   * 布隆過濾器
   */
  private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);


  public static void main(String[] args) {
    // 插入10萬樣本數據
    for (int i = 0; i < size; i++) {
      bloomFilter.put(i);
    }

    // 用另外十萬測試數據,測試誤判率
    int count = 0;
    for (int i = size; i < size + 100000; i++) {
      if (bloomFilter.mightContain(i)) {
        count++;
        System.out.println(i + "誤判了");
      }
    }
    System.out.println("總共的誤判數:" + count);
  }
}
復制代碼

運行結果:

10萬數據里有947個誤判,約等於0.01%,也就是我們代碼里設置的誤判率:fpp = 0.01。

深入分析代碼

核心BloomFilter.create方法

@VisibleForTesting
  static <T> BloomFilter<T> create(
      Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
    。。。。
}
復制代碼

這里有四個參數:

  • funnel:數據類型(一般是調用Funnels工具類中的)

  • expectedInsertions:期望插入的值的個數

  • fpp:誤判率(默認值為0.03)

  • strategy:哈希算法

我們重點講一下fpp參數

fpp誤判率

情景一:fpp = 0.01

  • 誤判個數:947

  • 占內存大小:9585058位數

情景二:fpp = 0.03(默認參數)

  • 誤判個數:3033

  • 占內存大小:7298440位數

情景總結

  • 誤判率可以通過fpp參數進行調節

  • fpp越小,需要的內存空間就越大:0.01需要900多萬位數,0.03需要700多萬位數。

  • fpp越小,集合添加數據時,就需要更多的hash函數運算更多的hash值,去存儲到對應的數組下標里。(忘了去看上面的布隆過濾存入數據的過程)

上面的numBits,表示存一百萬個int類型數字,需要的位數為7298440,700多萬位。理論上存一百萬個數,一個int是4字節32位,需要481000000=3200萬位。如果使用HashMap去存,按HashMap50%的存儲效率,需要6400萬位。可以看出BloomFilter的存儲空間很小,只有HashMap的1/10左右

上面的numHashFunctions表示需要幾個hash函數運算,去映射不同的下標存這些數字是否存在(0 or 1)。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM