大白話布隆過濾器

本文是站在小白的角度去討論布隆過濾器,若是你是科班出身,或者比較聰明,又或者真正想徹底搞懂布隆過濾器的能夠移步。java

不知道從何時開始,原本默默無聞的布隆過濾器一會兒名聲大燥,彷彿身在互聯網,作着開發的,無人不知,無人不曉,哪怕對技術不是很關心的小夥伴也聽過它的名號。我也花了很多時間去研究布隆過濾器,看了很多博客,無奈不是科班出身,又沒有那麼聰明的頭腦,又比較懶...通過「放棄,拿起,放棄,拿起」的無限輪迴,應該算是瞭解了布隆過濾器的核心思想,因此想給你們分享下。redis

布隆過濾器的應用

咱們先來看下布隆過濾器的應用場景,讓你們知道神奇的布隆過濾器到底能作什麼。數據庫

緩存穿透

咱們常常會把一部分數據放在Redis等緩存,好比產品詳情。這樣有查詢請求進來,咱們能夠根據產品Id直接去緩存中取數據,而不用讀取數據庫,這是提高性能最簡單,最廣泛,也是最有效的作法。通常的查詢請求流程是這樣的:先查緩存,有緩存的話直接返回,若是緩存中沒有,再去數據庫查詢,而後再把數據庫取出來的數據放入緩存,一切看起來很美好。可是若是如今有大量請求進來,並且都在請求一個不存在的產品Id,會發生什麼?既然產品Id都不存在,那麼確定沒有緩存,沒有緩存,那麼大量的請求都懟到數據庫,數據庫的壓力一會兒就上來了,還有可能把數據庫打死。 雖然有不少辦法均可以解決這問題,可是咱們的主角是「布隆過濾器」,沒錯,「布隆過濾器」就能夠解決(緩解)緩存穿透問題。至於爲何說是「緩解」,看下去你就明白了。數組

大量數據,判斷給定的是否在其中

如今有大量的數據,而這些數據的大小已經遠遠超出了服務器的內存,如今再給你一個數據,如何判斷給你的數據在不在其中。若是服務器的內存足夠大,那麼用HashMap是一個不錯的解決方案,理論上的時間複雜度能夠達到O(1),可是如今數據的大小已經遠遠超出了服務器的內存,因此沒法使用HashMap,這個時候就可使用「布隆過濾器」來解決這個問題。可是仍是一樣的,會有必定的「誤判率」。緩存

什麼是布隆過濾器

布隆過濾器是一個叫「布隆」的人提出的,它自己是一個很長的二進制向量,既然是二進制的向量,那麼顯而易見的,存放的不是0,就是1。bash

如今咱們新建一個長度爲16的布隆過濾器,默認值都是0,就像下面這樣: 服務器

image.png

如今須要添加一個數據:數據結構

咱們經過某種計算方式,好比Hash1,計算出了Hash1(數據)=5,咱們就把下標爲5的格子改爲1,就像下面這樣:函數

image.png

咱們又經過某種計算方式,好比Hash2,計算出了Hash2(數據)=9,咱們就把下標爲9的格子改爲1,就像下面這樣: 性能

image.png

仍是經過某種計算方式,好比Hash3,計算出了Hash3(數據)=2,咱們就把下標爲2的格子改爲1,就像下面這樣:

image.png

這樣,剛纔添加的數據就佔據了布隆過濾器「5」,「9」,「2」三個格子。

能夠看出,僅僅從布隆過濾器自己而言,根本沒有存放完整的數據,只是運用一系列隨機映射函數計算出位置,而後填充二進制向量。

這有什麼用呢?好比如今再給你一個數據,你要判斷這個數據是否重複,你怎麼作?

你只需利用上面的三種固定的計算方式,計算出這個數據佔據哪些格子,而後看看這些格子裏面放置的是否都是1,若是有一個格子不爲1,那麼就表明這個數字不在其中。這很好理解吧,好比如今又給你了剛纔你添加進去的數據,你經過三種固定的計算方式,算出的結果確定和上面的是如出一轍的,也是佔據了布隆過濾器「5」,「9」,「2」三個格子。

可是有一個問題須要注意,若是這些格子裏面放置的都是1,不必定表明給定的數據必定重複,也許其餘數據通過三種固定的計算方式算出來的結果也是相同的。這也很好理解吧,好比咱們須要判斷對象是否相等,是不能夠僅僅判斷他們的哈希值是否相等的。

也就是說布隆過濾器只能判斷數據是否必定不存在,而沒法判斷數據是否必定存在。

按理來講,介紹完了新增、查詢的流程,就要介紹刪除的流程了,可是很遺憾的是布隆過濾器是很難作到刪除數據的,爲何?你想一想,好比你要刪除剛纔給你的數據,你把「5」,「9」,「2」三個格子都改爲了0,可是可能其餘的數據也映射到了「5」,「9」,「2」三個格子啊,這不就亂套了嗎?

相信通過我這麼一介紹,你們對布隆過濾器應該有一個淺顯的認識了,至少你應該清楚布隆過濾器的優缺點了:

  • 優勢:因爲存放的不是完整的數據,因此佔用的內存不多,並且新增,查詢速度夠快;
  • 缺點: 隨着數據的增長,誤判率隨之增長;沒法作到刪除數據;只能判斷數據是否必定不存在,而沒法判斷數據是否必定存在。

能夠看到,布隆過濾器的優勢和缺點同樣明顯。

在上文中,我舉的例子二進制向量長度爲16,由三個隨機映射函數計算位置,在實際開發中,若是你要添加大量的數據,僅僅16位是遠遠不夠的,爲了讓誤判率下降,咱們還能夠用更多的隨機映射函數、更長的二進制向量去計算位置。

guava實現布隆過濾器

如今相信你對布隆過濾器應該有一個比較感性的認識了,布隆過濾器核心思想其實並不難,難的在於如何設計隨機映射函數,到底映射幾回,二進制向量的長度設置爲多少比較好,這可能就不是通常的開發能夠駕馭的了,好在Google大佬給咱們提供了開箱即用的組件,來幫助咱們實現布隆過濾器,如今就讓咱們看看怎麼Google大佬送給咱們的「禮物」吧。

首先在pom引入「禮物」:

<dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>19.0</version>
        </dependency>
複製代碼

而後就能夠測試啦:

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) {
        //插入數據
        for (int i = 0; i < 1000000; i++) {
            bloomFilter.put(i);
        }
        int count = 0;
        for (int i = 1000000; i < 2000000; i++) {
            if (bloomFilter.mightContain(i)) {
                count++;
                System.out.println(i + "誤判了");
            }
        }
        System.out.println("總共的誤判數:" + count);
    }
複製代碼

代碼簡單分析: 咱們定義了一個布隆過濾器,有兩個重要的參數,分別是 咱們預計要插入多少數據,咱們所指望的誤判率,誤判率不能爲0。 我向布隆過濾器插入了0-1000000,而後用1000000-2000000來測試誤判率。

運行結果:

1999501誤判了
1999567誤判了
1999640誤判了
1999697誤判了
1999827誤判了
1999942誤判了
總共的誤判數:10314
複製代碼

如今總共有100萬數據是不存在的,誤判了10314次,咱們計算下誤判率

image.png
和咱們定義的指望誤判率0.01相差無幾。

redis實現布隆過濾器

上面使用guava實現布隆過濾器是把數據放在本地內存中,沒法實現布隆過濾器的共享,咱們還能夠把數據放在redis中,用 redis來實現布隆過濾器,咱們要使用的數據結構是bitmap,你可能會有疑問,redis支持五種數據結構:String,List,Hash,Set,ZSet,沒有bitmap呀。沒錯,實際上bitmap的本質仍是String。

可能有小夥伴會說,納尼,布隆過濾器還沒介紹完,怎麼又出來一個bitmap,沒事,你能夠把bitmap就理解爲一個二進制向量。

要用redis來實現布隆過濾器,咱們須要本身設計映射函數,本身度量二進制向量的長度,這對我來講,無疑是一個不可能完成的任務,只能藉助搜索引擎,下面直接放出代碼把。

public class RedisMain {
    static final int expectedInsertions = 100;//要插入多少數據
    static final double fpp = 0.01;//指望的誤判率

    //bit數組長度
    private static long numBits;

    //hash函數數量
    private static int numHashFunctions;

    static {
        numBits = optimalNumOfBits(expectedInsertions, fpp);
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.0.109", 6379);
        for (int i = 0; i < 100; i++) {
            long[] indexs = getIndexs(String.valueOf(i));
            for (long index : indexs) {
                jedis.setbit("codebear:bloom", index, true);
            }
        }
        for (int i = 0; i < 100; i++) {
            long[] indexs = getIndexs(String.valueOf(i));
            for (long index : indexs) {
                Boolean isContain = jedis.getbit("codebear:bloom", index);
                if (!isContain) {
                    System.out.println(i + "確定沒有重複");
                }
            }
            System.out.println(i + "可能重複");
        }
    }

    /** * 根據key獲取bitmap下標 */
    private static long[] getIndexs(String key) {
        long hash1 = hash(key);
        long hash2 = hash1 >>> 16;
        long[] result = new long[numHashFunctions];
        for (int i = 0; i < numHashFunctions; i++) {
            long combinedHash = hash1 + i * hash2;
            if (combinedHash < 0) {
                combinedHash = ~combinedHash;
            }
            result[i] = combinedHash % numBits;
        }
        return result;
    }

    private static long hash(String key) {
        Charset charset = Charset.forName("UTF-8");
        return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();
    }

    //計算hash函數個數
    private static int optimalNumOfHashFunctions(long n, long m) {
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
    }

    //計算bit數組長度
    private 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)));
    }
}
複製代碼

運行結果:

88可能重複
89可能重複
90可能重複
91可能重複
92可能重複
93可能重複
94可能重複
95可能重複
96可能重複
97可能重複
98可能重複
99可能重複
複製代碼

本篇博客到這裏就結束了,謝謝你們。

相關文章
相關標籤/搜索