1. 程式人生 > 其它 >【布隆過濾器】一

【布隆過濾器】一

【布隆過濾器】一

解決快取穿透的辦法之一,就是布隆過濾器

快取穿透:指快取和資料庫中都沒有的資料,而使用者不斷髮起請求,如發起id為“-1”的資料或id為其他不存在的資料。這時的使用者很可能是攻擊者,攻擊會導致資料庫壓力過大。

布隆過濾器(Bloom Filter)

布隆過濾器(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,該資料存在集合中。(這裡有缺點,下面會講)

    可以通過乘法判斷是否有二進位制資料為0的。

刪除過程

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

布隆過濾器的優缺點

優點

  • 由於儲存的是二進位制資料,所以佔用的空間很小
  • 它的插入和查詢速度是非常快的,時間複雜度是O(K),可以聯想一下HashMap的過程
  • 保密性很好,因為本身不儲存任何原始資料,只有二進位制資料

缺點

新增資料是通過計算資料的hash值,那麼很有可能存在這種情況:兩個不同的資料計算得到相同的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>

二、程式碼實現

注意:布隆過濾器存在誤判率,可以手動設定,誤判率越小,佔用記憶體和hash計算耗時越大

package cn.lxiaol.www.bloomfilter;

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

public class BloomFilterCase {

    /**
     * 預計要存入多少資料
     */
    private static final int size = 1000000;

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

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


    private static final int total = 1000000;

    public static void main(String[] args) {

        // 插入100萬條樣本資料
        for (int i = 0; i < total; i++) {
            bloomFilter.put(i);
        }


        //用另外10萬測試誤判率
        int count = 0;
        for (int i = total; i < total+100000; i++) {
            if(bloomFilter.mightContain(i)){
                count++;
                System.out.println(i+"誤判了");
            }
        }

        System.out.println("總誤判數:"+count);
        System.out.println("fpp:" + 1.0 * count / 100000);

    }
}

10萬資料裡有947個誤判,約等於我們程式碼裡設定的誤判率:fpp = 0.01。

深入分析程式碼

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

        long numBits = optimalNumOfBits(expectedInsertions, fpp);
        int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);

        try {
            return new BloomFilter(new LockFreeBitArray(numBits), numHashFunctions, funnel, strategy);
        } catch (IllegalArgumentException var10) {
            throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", var10);
        }
    }

這裡有四個引數:

  • 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,大家就把它理解成上面說的二進位制結構。

程式碼實現

pom配置:

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

java程式碼:

package cn.lxiaol.www.bloomfilter;

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonBloomFilter {

    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:2020");
        config.useSingleServer().setPassword("yiguan789");

        // 構造redis
        RedissonClient redissonClient = Redisson.create(config);

        // 獲取布隆過濾器,並給該過濾器起一個名字,叫做 nickNameList
        RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter("nickNameList");

        // 初始化該過濾器,預計元素為 100萬,誤差率為3%  預設是3%
        bloomFilter.tryInit(1000000, 0.03);

        //將暱稱 "蹬杆老王子" 插入到過濾器中
        bloomFilter.add("蹬杆老王子");


        //判斷下面暱稱是否在布隆過濾器中
        //輸出false
        System.out.println(bloomFilter.contains("卡特機長"));
        //輸出true
        System.out.println(bloomFilter.contains("蹬杆老王子"));

        redissonClient.shutdown();
    }

}