最牛一篇布隆過濾器詳解

前言

咱們以前講了Redis的緩存雪崩、穿透、擊穿。在文章裏咱們說了解決緩存穿透的辦法之一,就是布隆過濾器,可是上次並無講如何使用布隆過濾器。java

做爲暖男的老哥,給大家補上,請叫我IT老暖男web

什麼是布隆過濾器

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

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

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

懂了嗎?做爲暖男的老哥在給大家畫張圖來幫助理解:數組

布隆過濾器用途

  • 解決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」。

由此得出以下缺點:

1、存在誤判

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

由於「你好」和「hello」的hash值是相同的,經過相同的hash值,找到的二進制數據也是同樣的,都是1。

2、刪除困難

到這裏我不說你們應該也明白爲何吧,做爲大家的暖男老哥,仍是講一下吧。

仍是用上面的舉例,由於「你好」和「hello」的hash值相同,對應的數組下標也是同樣的。

這時候老哥想去刪除「你好」,將下標爲2裏的二進制數據,由1改爲了0。

那麼咱們是否是連「hello」都一塊兒刪了呀。(0表明有這個數據,1表明沒有這個數據)

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

實現布隆過濾器

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

1、引入Guava pom配置

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>29.0-jre</version>
</dependency>

2、代碼實現

這裏咱們順便測一下它的誤判率。

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)。

解決Redis緩存雪崩

上面使用Guava實現的布隆過濾器是把數據放在了本地內存中。分佈式的場景中就不合適了,沒法共享內存。

咱們還能夠用Redis來實現布隆過濾器,這裏使用Redis封裝好的客戶端工具Redisson。

其底層是使用數據結構bitMap,你們就把它理解成上面說的二進制結構,因爲篇幅緣由,bitmap不在這篇文章裏講,咱們以後寫一篇文章介紹。

代碼實現

pom配置:

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.13.4</version>
</dependency>

java代碼:

public class RedissonBloomFilter {

  public static void main(String[] args) {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    config.useSingleServer().setPassword("1234");
    //構造Redisson
    RedissonClient redisson = Redisson.create(config);

    RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phoneList");
    //初始化布隆過濾器:預計元素爲100000000L,偏差率爲3%
    bloomFilter.tryInit(100000000L,0.03);
    //將號碼10086插入到布隆過濾器中
    bloomFilter.add("10086");

    //判斷下面號碼是否在布隆過濾器中
    //輸出false
    System.out.println(bloomFilter.contains("123456"));
    //輸出true
    System.out.println(bloomFilter.contains("10086"));
  }
}

因爲Guava那個版本,咱們已經很詳細的講了布隆過濾器的那些參數,這裏就不重複贅述了。


        
給個[在看],是對IT老哥最大的支持



本文分享自微信公衆號 - IT老哥(dys_family)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。