布隆過濾器應用,原理和性能分析

1.應用

布隆過濾器用於判斷某一個值是不是已經存在。比如我們在使用新聞客戶端看新聞時,它會給我們不停地推薦新的內容,而它每次推薦時都要去重,以去掉那些我們已經看過的內容。

布隆過濾器是專門用來解決這種去重問題的,它在起到去重作用的同時,在空間上還能節省90%以上,但是會有一定的誤判概率。當布隆過濾器說某個值存在時,這個值可能不存在;當它說某個值不存在時,那就肯定不存在。

用在推送去重的場景中,布隆過濾器可以準確地過濾掉那些用戶已經看過的內容,用戶沒有看過的內容,它也會過濾掉一小部分(誤判),這樣就可以保證推薦給用戶的內容都是無重複的。

2.原理

1.布隆過濾器數據結構

布隆過濾器是一個 bit 向量或者說 bit 數組,長這樣:

如果我們要映射一個值到布隆過濾器中,我們需要使用多個不同的哈希函數生成多個哈希值,並對每個生成的哈希值指向的 bit 位置 1,例如針對值 “baidu” 和三個不同的哈希函數分別生成了哈希值 1、4、7,則上圖轉變爲:

Ok,我們現在再存一個值 “tencent”,如果哈希函數返回 3、4、8 的話,圖繼續變爲:

值得注意的是,4 這個 bit 位由於兩個值的哈希函數都返回了這個 bit 位,因此它被覆蓋了。現在我們如果想查詢 “dianping” 這個值是否存在,哈希函數返回了 1、5、8三個值,結果我們發現 5 這個 bit 位上的值爲 0,說明沒有任何一個值映射到這個 bit 位上,因此我們可以很確定地說 “dianping” 這個值不存在。而當我們需要查詢 “baidu” 這個值是否存在的話,那麼哈希函數必然會返回 1、4、7,然後我們檢查發現這三個 bit 位上的值均爲 1,那麼我們可以說 “baidu” 存在了麼?答案是不可以,只能是 “baidu” 這個值可能存在。

這是爲什麼呢?答案跟簡單,因爲隨着增加的值越來越多,被置爲 1 的 bit 位也會越來越多,這樣某個值 “taobao” 即使沒有被存儲過,但是萬一哈希函數返回的三個 bit 位都被其他值置位了 1 ,那麼程序還是會判斷 “taobao” 這個值存在。

2.如何選擇哈希函數個數和布隆過濾器長度

很顯然,過小的布隆過濾器很快所有的 bit 位均爲 1,那麼查詢任何值都會返回“可能存在”,起不到過濾的目的了。布隆過濾器的長度會直接影響誤報率,布隆過濾器越長其誤報率越小。

另外,哈希函數的個數也需要權衡,個數越多則布隆過濾器 bit 位置位 1 的速度越快,且布隆過濾器的效率越低;但是如果太少的話,那我們的誤報率會變高。

k 爲哈希函數個數,m 爲布隆過濾器長度,n 爲插入的元素個數,p 爲誤報率

如何選擇適合業務的 k 和 m 值呢,這裏直接貼一個公式:

3.性能分析

1.時間和內存分析

redis 布隆過濾器提供的指令有四個。

  • bf.add添加元素
  • bf.exists查詢元素是否存在
  • bf.madd批量添加元素
  • bf.mexists一次查詢多個元素是否存在

和redis基本類型set的部分功能類似,其中bf.add和bf.madd指令可以類比set集合的sadd指令,bf.exists可以類比set集合的sismember指令。這裏對bf.add和bf.exists的性能與set集合進行對比測試。

使用Java編寫一小段測試程序,由於jedis不支持布隆過濾器,可以使用RedisLabs提供的JReBloom:

public class BloomFilterTest {
    private static final Jedis  jedis       = new Jedis("127.0.0.1", 6379);
    private static final String VALUE_PRE   = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789";
    private static final String TEST_KEY    = "test.key";
    private static final String TEST_BF_KEY = "test.bf.key";
    private static final Client client      = new Client("127.0.0.1", 6379);

    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.println("輸入測試條數:");
        int times = scan.nextInt();
        System.out.println("輸入測試指令:");
        String order = scan.next();
        System.out.println("----start test----");

        long start = System.currentTimeMillis();
        if ("sadd".equals(order)) {
            sadd(times);
        } else if ("bfadd".equals(order)) {
            bfadd(times);
        } else if ("bfexists".equals(order)) {
            bfexists(times);
        } else if ("sismember".equals(order)) {
            sismember(times);
        }

        System.out.println(order + "共耗時" + (System.currentTimeMillis() - start) + "ms");
        jedis.close();
        client.close();
    }

    private static void sismember(int times) {
        for (int i=0; i<times; i++) {
            String item = generateString(i);
            jedis.sismember(TEST_KEY, item);
        }
    }

    private static void bfexists(int times) {
        for (int i=0; i<times; i++) {
            String item = generateString(i);
            client.exists(TEST_BF_KEY, item);
        }
    }

    private static void bfadd(int times) {
        for (int i=0; i<times; i++) {
            String item = generateString(i);
            client.add(TEST_BF_KEY, item);
        }
    }

    private static void sadd(int times) {
        for (int i=0; i<times; i++) {
            String item = generateString(i);
            jedis.sadd(TEST_KEY, item);
        }
    }

    private static String generateString(int i) {
        return VALUE_PRE + i;
    }
}

測試流程:

首先,使用redis的redis-cli info memory命令看下當前redis的內存情況

編譯運行java程序,插入100w條記錄進行測試,value大小在30個字符左右。

1.bf.add指令

內存佔用情況

2.sadd指令

內存佔用

可以看出,bf.add指令與sadd指令在時間上相比並沒有優勢,但是在內存佔用上來說,set集合的內容佔用大概是布隆過濾器的10倍。

3.sismember指令

4.bf.exists指令

2.誤差分析

稍微修改下上面的程序

public class BloomFilterError {
    private static final Jedis  jedis       = new Jedis("127.0.0.1", 6379);
    private static final String VALUE_PRE   = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789";
    private static final String TEST_BF_KEY = "test.bf.key";
    private static final Client client      = new Client("127.0.0.1", 6379);

    public static void main(String[] args) {
        for (long i=100000; i<=1000000;) {
            int bfexists = bfexists(i);
            System.out.printf("count=" + i + "\tbfexists=" + bfexists);
            System.out.printf("\terror percentages=%.3f\n", Math.abs(i - bfexists)/(i*0.01));
            i += 100000;
        }

        jedis.close();
        client.close();
    }

    private static int bfexists(long times) {
        int correct = 0;
        for (int i=0; i<times; i++) {
            String item = generateString(i);
            if (client.exists(TEST_BF_KEY, item)) {
                correct++;
            }
        }
        return correct;
    }

    private static String generateString(int i) {
        return VALUE_PRE + i;
    }
}

運行結果:

可以看到存在的正確率是100%。

修改下generateString的邏輯,生成的全是布隆過濾器中不存在的值:

private static String generateString(int i) {
    return VALUE_PRE + "0" + i;
}

運行程序,看下判斷的正確率:

可以看到正確率都在98.6%以上。

plus

誤判率可以使用公式計算,要求誤判率越低,所需要的空間越大。如果一千萬的數據,誤判率允許 1%, 大概需要11M左右;如果要求誤判率爲 0.1%,則大概需要 17 M左右。可以使用bloom filter calculator計算。https://krisives.github.io/bloom-calculator/

參考博文:

https://zhuanlan.zhihu.com/p/43263751

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