【redis知識點整理】 --- 從guava源碼的角度簡單聊聊布隆過濾器

本文代碼對應的github地址:https://github.com/nieandsun/redis-study


前段時間項目里加上了布隆過濾器,本文簡單從guava源碼的角度做一些分析 —》 其實主要是爲自己答疑解惑!!!



1 布隆過濾器在互聯網環境的使用場景簡介

我覺得布隆過濾器在互聯網環境的使用場景用下面這幅圖就可以描述的很清楚,這裏我就不過多去敘述了。
在這裏插入圖片描述
當然有些項目,或許也會這樣使用布隆過濾器:
在這裏插入圖片描述


2 布隆過濾器的原理

這裏直接引用文章https://www.jianshu.com/p/bef2ec1c361f對布隆過濾器原理的敘述:

BF是由一個長度爲m比特的位數組(bit array)k個哈希函數(hash function)組成的數據結構。位數組均初始化爲0,所有哈希函數都可以分別把輸入數據儘量均勻地散列。


當要插入一個元素時,將其數據分別輸入k個哈希函數,產生k個哈希值。以哈希值作爲位數組中的下標,將所有k個對應的比特置爲1。


當要查詢(即判斷是否存在)一個元素時,同樣將其數據輸入哈希函數,然後檢查對應的k個比特。如果有任意一個比特爲0,表明該元素一定不在集合中。如果所有比特均爲1,表明該集合有(較大的)可能性在集合中。爲什麼不是一定在集合中呢?因爲一個比特被置爲1有可能會受到其他元素的影響,這就是所謂“假陽性”(false positive)。相對地,“假陰性”(false negative)在BF中是絕不會出現的。


下圖示出一個m=18, k=3的BF示例。集合中的x、y、z三個元素通過3個不同的哈希函數散列到位數組中。當查詢元素w時,因爲有一個比特爲0,因此w不在該集合中。
在這裏插入圖片描述
在這裏插入圖片描述


3 我對 布隆過濾器 困惑 + 思考

說實話其實布隆過濾器的原理,我在上學的時候就知道了,但是剛看這個原理的時候,不知道你有沒有過這樣的困惑 + 思考過程:

(1)首先應該明確的是,當你拿着一個值去布隆過濾器裏去查詢時,只要布隆過濾器拿着你這個值有一次hash運算發現對應位置的值爲0, 則就可以明確的說明,該值不存在 —> 這點是毋庸置疑的!!!

(2)問題在於,假設數組裏有很多位置都變爲1了,那錯判率(又叫容錯率)豈不會很大 —> 因此,可以想像爲了降低 容錯率,肯定是數組越長越好 —> 但是肯定又不能無限長,因爲那樣佔用的存儲空間就會很大!!!

(3)還有就是hash函數的個數,如果只有一個hash函數,那就很有可能由於hash碰撞而發生錯判 —> 但是如果hash函數過多,那由0變爲1的位置也會變多 —> 這樣好像也會致使錯判率升高!!! —> 因此hash函數的個數,到底怎樣纔算合理呢???

這玩意,我覺得單靠自己去想,尤其是第(3)個問題,我覺得抓破頭皮也不一定能想明白 —> 還好,Google的工程師替我們去想了,並將布隆過濾器的算法,集成到了guava包裏!!!


4 布隆過濾器在guava中具體是個啥

首先guava包,想必大家應該都知道,其maven依賴如下:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>19.0</version>
</dependency>

4.1 通過與HashMap對比,簡單理解一下guava中的布隆過濾器到底是什麼

布隆過濾器按照我的理解,它其實就是一個算法 + 數據結構的結合體,其本質其實是一個容器。

我覺得其實可以將其與HashMap進行類比:

  • 比如說我們可以說HashMap也是一個算法 + 數據結構構成的容器,其底層用到了數組、鏈表、紅黑樹等數據結構,往map容器裏put值用到了hash算法,爲了降低hash碰撞又用到了高16位低16位進行與運算的算法等等 || 從map裏取值又用到了數組遍歷、鏈表遍歷、紅黑樹遍歷對應的算法等等

  • 與之類似, 布隆過濾器也是一個算法 + 數據結構構成的容器,其底層的數據結構就是一個bit數組,往布隆過濾器裏放值的過程,其實就是拿着該值經過指定次數的hash運算 ,並將運算結果對應位置的值由0改爲1(不存儲該值,這裏一定要注意) || 驗證某個值是否在布隆過濾器裏時,就拿着該值再次經過指定次數的hash運算,並看看運算結果對應的位置是否有爲0的,只要有一個hash運算結果對應的位置爲0,就表明該值沒有往布隆過濾器裏放過。


4.2 guava中布隆過濾器API簡介


4.2.1 初始化一個布隆過濾器的方式 — 類似於new一個HashMap

初始化一個布隆過濾器的方法有如下幾個:
在這裏插入圖片描述
以上面第3個create方法爲例進行一下簡單介紹:

  • 第一個參數: 用來指定往布隆過濾器裏放的值的數據類型
  • 第二個參數: 用來指定往布隆過濾器裏放多少個值,相當於新建HashMap時指定初識容量
  • 第三個參數:指定錯判率,也就是容錯率 — 注意不能爲0
  • 第四個參數:進行hash運算的策略

也就是說初始化一個布隆過濾器,必須要指定的兩個參數是

  • 往布隆過濾器裏放的值的數據類型
  • 往布隆過濾器裏放多少個值

4.2.2 往布隆過濾器裏放值的方式

特別簡單,即調用布隆過濾器的put方法

4.2.3 判斷某個值是否在布隆過濾器裏

也特別簡單,就是調用布隆過濾器的contains方法

4.2.4 通過一個簡單的栗子,看一下布隆過濾器的API使用姿勢

public class BloomFilterTest {
    private static final int insertions = 1000000;

    @Test
    public void bfTest() {
        //初始化一個存儲String數據的布隆過濾器,初始化大小100W
        BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions);


        //初始化一個存儲String數據的set,初始化大小100w
        //Set<String> sets = new HashSet<>(insertions);
        Set<String> sets = Sets.newHashSetWithExpectedSize(insertions); //用guava進行new HashSet() 的方式,與上面的代碼一個意思

        //初始化一個存儲String數據的set,初始化大小100w
        //List<String> lists = new ArrayList<String>(insertions);
        ArrayList<String> lists = Lists.newArrayListWithCapacity(insertions);//用guava進行new HashSet() 的方式,與上面的代碼一個意思

        //向三個容器初始化100萬個隨機並且唯一的字符串 , 100萬個uuid 34M多
        for (int i = 0; i < insertions; i++) {
            String uuid = UUID.randomUUID().toString();
            bf.put(uuid);
            sets.add(uuid);
            lists.add(uuid);
        }
        int wrong = 0;//布隆過濾器錯誤判斷的次數
        int right = 0;//布隆過濾器正確判斷的次數

        /****
         * 相信你耷眼一看就知道,這10000次循環裏,會有100個肯定是在布隆過濾器裏存在的
         * 剩下10000 - 100個肯定是不在布隆過濾器裏的
         */
        for (int i = 0; i < 10000; i++) {

            String test = i % 100 == 0 ? lists.get(i / 100) : UUID.randomUUID().toString();//按照一定比例選擇bf中肯定存在的值

            if (bf.mightContain(test)) {
                if (sets.contains(test)) {
                    right++;
                } else {
                    wrong++;
                }
            }
        }

        System.out.println("=============right=============" + right);
        System.out.println("=============誤判率=============" + wrong / 10000.0);
    }
}

簡單看一眼運行結果:
在這裏插入圖片描述


5 簡單從guava源碼的角度爲自己解解惑

再回過頭去看我在本文第3小節的疑惑 與 思考,其實我們不難發現,要想真正把布隆過濾器的思想進行落地,最最重要的肯定就是要解決兩個問題:

  • ① 在某個容錯率範圍內,bit數組究竟多大合適???
  • ② 在某個容錯率範圍內,hash運算多少次比較合適???

我們拿着 4.2.4 中的代碼打着斷點看一下:


(1)首先在調用create進行初始化布隆過濾器時,可以看到默認情況下布隆過濾器的容錯率爲0.03 —》 這就解釋了爲什麼我們4.2.4中的錯判率約爲0.03的原因。
在這裏插入圖片描述


(2)繼續跟斷點,可以在BloomFilter源碼的350、351兩行看到我們想要的答案
在這裏插入圖片描述
這裏貼一下計算bit合理長度以及hash合理次數的源碼:

  /**
   * Computes m (total bits of Bloom filter) which is expected to achieve, for the specified
   * expected insertions, the required false positive probability.
   *
   * See http://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives for the formula.
   *
   * @param n expected insertions (must be positive)
   * @param p false positive rate (must be 0 < p < 1)
   */
  @VisibleForTesting
  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)));
  }
  /**
   * Computes the optimal k (number of hashes per element inserted in Bloom filter), given the
   * expected insertions and total number of bits in the Bloom filter.
   *
   * See http://en.wikipedia.org/wiki/File:Bloom_filter_fp_probability.svg for the formula.
   *
   * @param n expected insertions (must be positive)
   * @param m total number of bits in Bloom filter (must be positive)
   */
  @VisibleForTesting
  static int optimalNumOfHashFunctions(long n, long m) {
    // (m / n) * log(2), but avoid truncation due to division!
    return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
  }

我貼這兩段代碼想說明啥呢?

  • ① 其實原理我看不懂
  • ② 如若你想研究,可以看人家註釋中給出的科學依據
  • ③ 雖然我看不懂,但是我卻猜到了它的存在(哈哈。。。)

(3)接着我們來看一下它計算出的結果:
numBits = 7298440
numHashFunctions = 5
這表明 : 在布隆過濾器裏放置1千萬個數據,我們隨機拿n個數據,判斷這n個數據是否在布隆過濾器裏存過,在錯判率爲0.03時,需要7298440位的內存大小,經過換算後可以發現,不到1M。
在這裏插入圖片描述


6 互聯網場景使用布隆過濾器的可行性分析

對4.2.4中的代碼稍作修改,我們假設我們的數據量有1千萬,規定的容錯率爲:0.00001,則才需要用到的內存大小爲239626459位,經過換算後如下:
在這裏插入圖片描述
這是什麼概念呢?
(1)假設我們的數據庫裏有1千萬個值 —> 這其實應該是一個比較大的數字了

這裏看一下阿里的《Java開發手冊》,可知數據庫單錶行數據超過500行,就可以考慮分庫分表了:
在這裏插入圖片描述

(2) 我們只需要28.6M的內存就可以保證:同一時間10萬個惡意請求,只有一個左右的請求可以真正到達我們的數據庫。。。

(3)說到這裏,其實還有一個問題:或許有些互聯網公司,會搭建比較龐大的redis集羣,此時redis內某類數據的數據量可能會遠遠大於1千萬,此時要萬一有人就是要攻擊你的redis集羣,那第1小節中第二個圖的架構模式,就非常有必要了,有興趣的可以clone下來代碼,自己測算一下。

由此可知,布隆過濾器在解決緩存擊穿問題上確實非常有效!!!


最後提醒一下,請格外注意一下,第5小節,註釋掉的那段話
end!!!

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