布隆過濾器

本文是站在小白的角度去討論布隆過濾器,如果你是科班出身,或者比較聰明,又或者真正想完全搞懂布隆過濾器的可以移步。

不知道從什麼時候開始,本來默默無聞的布隆過濾器一下子名聲大燥,彷彿身在互聯網,做着開發的,無人不知,無人不曉,哪怕對技術不是很關心的小夥伴也聽過它的名號。我也花了不少時間去研究布隆過濾器,看了不少博客,無奈不是科班出身,又沒有那麼聰明的頭腦,又比較懶...經過“放棄,拿起,放棄,拿起”的無限輪迴,應該算是瞭解了布隆過濾器的核心思想,所以想給大家分享下。

布隆過濾器的應用

我們先來看下布隆過濾器的應用場景,讓大家知道神奇的布隆過濾器到底能做什麼。

緩存穿透

我們經常會把一部分數據放在Redis等緩存,比如產品詳情。這樣有查詢請求進來,我們可以根據產品Id直接去緩存中取數據,而不用讀取數據庫,這是提升性能最簡單,最普遍,也是最有效的做法。一般的查詢請求流程是這樣的:先查緩存,有緩存的話直接返回,如果緩存中沒有,再去數據庫查詢,然後再把數據庫取出來的數據放入緩存,一切看起來很美好。但是如果現在有大量請求進來,而且都在請求一個不存在的產品Id,會發生什麼?既然產品Id都不存在,那麼肯定沒有緩存,沒有緩存,那麼大量的請求都懟到數據庫,數據庫的壓力一下子就上來了,還有可能把數據庫打死。
雖然有很多辦法都可以解決這問題,但是我們的主角是“布隆過濾器”,沒錯,“布隆過濾器”就可以解決(緩解)緩存穿透問題。至於爲什麼說是“緩解”,看下去你就明白了。

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

現在有大量的數據,而這些數據的大小已經遠遠超出了服務器的內存,現在再給你一個數據,如何判斷給你的數據在不在其中。如果服務器的內存足夠大,那麼用HashMap是一個不錯的解決方案,理論上的時間複雜度可以達到O(1),但是現在數據的大小已經遠遠超出了服務器的內存,所以無法使用HashMap,這個時候就可以使用“布隆過濾器”來解決這個問題。但是還是同樣的,會有一定的“誤判率”。

什麼是布隆過濾器

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

現在我們新建一個長度爲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相差無幾。
 

緩存穿透

即黑客故意去請求緩存中不存在的數據,導致所有的請求都懟到數據庫上,從而數據庫連接異常。

解決這類問題的方法

方法一:當DB和redis中都不存在key,在DB返回null時,在redis中插入<key,null,expireTime>當key再次請求時,redis直接返回null,而不用再次請求DB。

方法二:使用redis提供的redisbloom,同樣是將存在的key放入到過濾器中。當請求進來時,先去過濾器中校驗是否存在,如果不存在直接返回null。
å¨è¿éæå¥å¾çæè¿°

過濾器用途

  1. 判斷過濾器中是否存在該數據進而減少沒有必要的數據庫請求

引入redisbloom

官方文檔上面提供docker安裝redisbloom和下載編譯的方式引入redisbloom的模塊。
下面介紹一下編譯的方式來引入redisbloom模塊

git clone https://github.com/RedisLabsModules/redisbloom.git
cd redisbloom
make # 編譯redisbloom

啓動redis-server時引入redisbloom模塊

./redis-5.0.4/src/redis-server --loadmodule ./redisbloom/rebloom.so

在redis.conf中配置

loadmodule ../redisbloom/rebloom.so

bloom 指令

bf.reserve {key} {error_rate} {size}

127.0.0.1:6379> bf.reserve userid 0.01 100000
OK

描述:
創建一個空的布隆過濾器,並設置一個期望的錯誤率和初始大小。{error_rate}過濾器的錯誤率在0-1之間,如果要設置0.1%,則應該是0.001。該數值越接近0,內存消耗越大,對cpu利用率越高。
bf.add {key} {item}

bf.madd {key} {item} [item…]

描述:往過濾器中添加元素。如果key不存在,過濾器會自動創建。
 

127.0.0.1:6379> bf.add userid '101310299'
(integer) 1
127.0.0.1:6379> bf.madd userid '101310299' '101310366' '101310211'
1) (integer) 0
2) (integer) 1
3) (integer) 1

bf.exists {key} {item}

bf.mexists {key} {item} [item…]

  • 描述:判斷過濾器中是否存在該元素,不存在返回0,存在返回1。
127.0.0.1:6379> bf.exists userid '101310299'
(integer) 1
127.0.0.1:6379> bf.mexists userid '101310299' '10saa' '101310211'
1) (integer) 1
2) (integer) 0
3) (integer) 1

java API

java程序員可以通過RedisBloom類庫提供的API實現高性能布隆過濾器

maven引入類庫

	<dependency>
      <groupId>com.redislabs</groupId>
      <artifactId>jrebloom</artifactId>
      <version>1.0.1</version>
    </dependency>
    <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.0.0</version>
        </dependency>

API使用

public class RedisBloomDemo {
    public static void main(String[] args) {
        String userIdBloomKey = "userid";
        // 創建客戶端,jedis實例
        Client client = new Client("localhost", 6378);
        // 創建一個有初始值和出錯率的過濾器
        client.createFilter(userIdBloomKey,100000,0.01);
        // 新增一個<key,value>
        boolean userid1 = client.add(userIdBloomKey,"101310222");
        System.out.println("userid1 add " + userid1);

        // 批量新增values
        boolean[] booleans = client.addMulti(userIdBloomKey, "101310111", "101310222", "101310222");
        System.out.println("add multi result " + booleans);

        // 某個value是否存在
        boolean exists = client.exists(userIdBloomKey, "101310111");
        System.out.println("101310111 是否存在" + exists);

        //某批value是否存在
        boolean existsBoolean[] = client.existsMulti(userIdBloomKey, "101310111","101310222", "101310222","11111111");
        System.out.println("某批value是否存在 " + existsBoolean);
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章