1. 程式人生 > 實用技巧 >玩轉Redis-Redis中布隆過濾器的使用及原理

玩轉Redis-Redis中布隆過濾器的使用及原理

  《玩轉Redis》系列文章主要講述Redis的基礎及中高階應用。本文是《玩轉Redis》系列第【11】篇,最新系列文章請前往公眾號“zxiaofan”檢視,或百度搜索“玩轉Redis zxiaofan”即可。

往期精選:《玩轉Redis-HyperLogLog原理探索》

本文關鍵字:玩轉Redis、Bloom filter、布隆過濾器、無偏hash函式;

大綱

  • 布隆過濾器介紹
    • 什麼是布隆過濾器
    • 布隆過濾器有什麼特性
  • Redis布隆過濾器實戰
    • rebloom的安裝
    • 布隆過濾器的命令詳解及示例
  • 布隆過濾器的底層原理
    • 布隆過濾器的底層結構
    • 最佳hash函式數量與錯誤率的關係
    • 所需儲存空間與錯誤率及容量關係
    • 布隆過濾器如何擴容
  • 布隆過濾器有哪些應用場景
  • 布隆過濾器的優缺點
  • 延伸拓展

1、布隆過濾器介紹

  先前我們學習了HyperLogLog(傳送門《玩轉Redis-HyperLogLog原理探索》《玩轉Redis-HyperLogLog統計微博日活月活》),非常適合大資料下的基數計算場景,但其有個缺陷,無法判斷某個值是否已存在。

  Hash、Set、String的BitMap等可以實現判斷元素是否存在的功能,但這些實現方式要麼隨著元素增多會佔用大量記憶體(Hash、Set),要麼無法動態伸縮和保持誤判率不變(BitMap)。因此,我們非常需要一種可以高效判斷大量資料是否存在且允許一定誤判率的資料結構。

1.1、什麼是布隆過濾器(Bloom Filter)

  布隆過濾器由Burton Howard Bloom於1970年提出,用於判斷一個元素是否在集合中。

  布隆過濾器(Bloom filter)是一種非常節省空間的概率資料結構(space-efficient probabilistic data structure),執行速度快(時間效率),佔用記憶體小(空間效率),但是有一定的誤判率且無法刪除元素。本質上由一個很長的二進位制向量和一系列隨機對映函式組成。

1.2 布隆過濾器有什麼特性

  • 檢查一個元素是否在整合中;
  • 檢查結果分為2種:一定不在集合中、可能在集合中;
  • 布隆過濾器支援新增元素、檢查元素,但是不支援刪除元素;
  • 檢查結果的“可能在集合中”說明存在一定誤判率;
    • 已經新增進入布隆過濾器的元素是不會被誤判的,僅未新增過的元素才可能被誤判;
  • 相比set、Bitmaps非常節省空間:因為只儲存了指紋資訊,沒有儲存元素本身;
  • 新增的元素超過預設容量越多,誤報的可能性越大。

2、Redis布隆過濾器實戰

2.1、rebloom的安裝

   還沒有安裝Redis的同學,可以參考我先前的文章安裝,傳送門《玩轉Redis-Redis安裝、後臺啟動、解除安裝》。Redis 4.0開始以外掛形式提供布隆過濾器。

# docker方式安裝

> docker pull redislabs/rebloom  # 拉取映象
> docker run -p6379:6379 redislabs/rebloom  # 執行容器
> redis-cli  # 連線容器中的 redis 服務
# linux伺服器直接安裝

>git clone git://github.com/RedisLabsModules/rebloom
>cd rebloom
>make
# 當前路徑會生成一個rebloom.so檔案
# 在redis的配置中(通常在/etc/redis/redis.conf)增加一行配置 loadmodule /"rebloom.so的絕對路徑"/rebloom.so
# 重啟Redis即可

   上述的安裝提到需要重啟Redis,但是生產環境的Redis可不是你想重啟就重啟的。有什麼方式可以不重啟Redis就載入rebloom外掛嗎,MODULE LOAD命令就派上用場了。

# 不重啟Redis載入rebloom外掛

1、檢視redis當前已載入的外掛
> MODULE LOAD /"rebloom.so的絕對路徑"/redisbloom.so
> module list
1) 1) "name"
   2) "bf"
   3) "ver"
   4) (integer) 999999
# 看到以上資料則說明redisbloom載入成功了,模組名name為"bf",模組版本號ver為999999。

# 動態執行模組解除安裝
# MODULE UNLOAD 模組名

# 當然,為了防止Redis重啟導致動態載入的模組丟失,我們還是應該在redis.conf 中加上相關配置。

2.2、布隆過濾器的命令詳解及示例

完整指令說明可前往官網檢視:https://oss.redislabs.com/redisbloom/Bloom_Commands/。

2.2.1、Bloom命令簡述

【核心命令】新增元素:BF.ADD(新增單個)、BF.MADD(新增多個)、BF.INSERT(新增多個);

【核心命令】檢查元素是否存在:BF.EXISTS(查詢單個元素)、BF.MEXISTS(查詢多個元素)

命令功能引數
BF.RESERVE 建立一個大小為capacity,錯誤率為error_rate的空的Bloom BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion] [NONSCALING]
BF.ADD 向key指定的Bloom中新增一個元素item BF.ADD {key} {item}
BF.MADD 向key指定的Bloom中新增多個元素 BF.MADD {key} {item} [item...]
BF.INSERT 向key指定的Bloom中新增多個元素,新增時可以指定大小和錯誤率,且可以控制在Bloom不存在的時候是否自動建立 BF.INSERT {key} [CAPACITY {cap}] [ERROR {error}] [EXPANSION expansion] [NOCREATE] [NONSCALING] ITEMS {item...}
BF.EXISTS 檢查一個元素是否可能存在於key指定的Bloom中 BF.EXISTS {key} {item}
BF.MEXISTS 同時檢查多個元素是否可能存在於key指定的Bloom中 BF.MEXISTS {key} {item} [item...]
BF.SCANDUMP 對Bloom進行增量持久化操作 BF.SCANDUMP {key} {iter}
BF.LOADCHUNK 載入SCANDUMP持久化的Bloom資料 BF.LOADCHUNK {key} {iter} {data}
BF.INFO 查詢key指定的Bloom的資訊 BF.INFO {key}
BF.DEBUG 檢視BloomFilter的內部詳細資訊(如每層的元素個數、錯誤率等) BF.DEBUG {key}

2.2.2、BF.RESERVE

  • 引數
    • BF.RESERVE {key} {error_rate} {capacity}
  • 功能
    • 建立一個大小為capacity,錯誤率為error_rate的空的BloomFilter
  • 時間複雜度
    • O(1)
  • 引數說明
    • key:布隆過濾器的key;
    • error_rate:期望的錯誤率(False Positive Rate),該值必須介於0和1之間。該值越小,BloomFilter的記憶體佔用量越大,CPU使用率越高。
    • capacity:布隆過濾器的初始容量,即期望新增到布隆過濾器中的元素的個數。當實際新增的元素個數超過該值時,布隆過濾器將進行自動的擴容,該過程會導致效能有所下降,下降的程度是隨著元素個數的指數級增長而線性下降。
  • 可選引數
    • expansion:當新增到布隆過濾器中的資料達到初始容量後,布隆過濾器會自動建立一個子過濾器,子過濾器的大小是上一個過濾器大小乘以expansion。expansion的預設值是2,也就是說布隆過濾器擴容預設是2倍擴容。
    • NONSCALING:設定此項後,當新增到布隆過濾器中的資料達到初始容量後,不會擴容過濾器,並且會丟擲異常((error) ERR non scaling filter is full)。
  • 返回值
    • 成功:OK;
    • 其它情況返回相應的異常資訊。
  • 備註
    • BloomFilter的擴容是通過增加BloomFilter的層數來完成的。每增加一層,在查詢的時候就可能會遍歷多層BloomFilter來完成,每一層的容量都是上一層的兩倍(預設)。
# 公眾號@zxiaofan
# 建立一個容量為5且不允許擴容的過濾器;
127.0.0.1:6379> bf.reserve bf2 0.1 5 NONSCALING
OK
127.0.0.1:6379> bf.madd bf2 1 2 3 4 5
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1

# 新增第6個元素時即提示BloomFilter已滿;
127.0.0.1:6379> bf.madd bf2 6
1) (error) ERR non scaling filter is full
127.0.0.1:6379> bf.info bf2
 1) Capacity
 2) (integer) 5
 3) Size
 4) (integer) 155
 5) Number of filters
 6) (integer) 1
 7) Number of items inserted
 8) (integer) 5
 9) Expansion rate
10) (integer) 2

2.2.3、BF.ADD

  • 引數
    • BF.ADD {key} {item}
  • 功能
    • 向key指定的Bloom中新增一個元素item。
  • 時間複雜度
    • O(log N),N是過濾器的層數。
  • 引數說明
    • key:布隆過濾器的名字;
    • item:待插入過濾器的元素;
  • 返回值
    • 元素不存在插入成功:返回1;
    • 元素可能已經存在:返回0;
    • 其它情況返回相應的異常資訊。

2.2.3、BF.MADD

  • 引數
    • BF.MADD {key} {item} [item...]
  • 功能
    • 向key指定的Bloom中新增多個元素item。
  • 時間複雜度
    • O(log N),N是過濾器的層數。
  • 引數說明
    • key:布隆過濾器的名字;
    • item:待插入過濾器的元素,可插入多個;
  • 返回值
    • 成功:返回一個數組,陣列的每一個元素可能為1或0,當item一定不存在時陣列元素值為1,當item可能已經存在時陣列元素值為0。
    • 其它情況返回相應的異常資訊。

2.2.5、BF.EXISTS

  • 引數
    • BF.EXISTS {key} {item}
  • 功能
    • 檢查一個元素是否可能存在於key指定的Bloom中
  • 時間複雜度
    • O(log N),N是過濾器的層數。
  • 引數說明
    • key:布隆過濾器的名字;
    • item:待檢查的元素;
  • 返回值
    • 元素一定不存在:0;
    • 元素可能存在:1;
    • 其它情況返回相應的異常資訊。

2.2.6、BF.MEXISTS

  • 引數
    • BF.MEXISTS <key> <item> [item...]
  • 功能
    • 檢查多個元素是否可能存在於key指定的Bloom中
  • 時間複雜度
    • O(log N),N是過濾器的層數。
  • 引數說明
    • key:布隆過濾器的名字;
    • item:待檢查的元素,可設定多個;
  • 返回值
    • 成功:返回一個數組,陣列的每一個元素可能為1或0,當item一定不存在時陣列元素值為0,當item可能已經存在時陣列元素值為1。
    • 其它情況返回相應的異常資訊。
# 公眾號@zxiaofan
# 向BloomFilter新增單個元素
127.0.0.1:6379&gt; bf.add bf1 itemadd1
(integer) 1

# 向BloomFilter批量新增多個元素
127.0.0.1:6379&gt; bf.madd bf1 itemmadd1 itemmadd2
1) (integer) 1
2) (integer) 1
127.0.0.1:6379&gt; bf.exists itemmadd1
(error) ERR wrong number of arguments for 'bf.exists' command
127.0.0.1:6379&gt; bf.exists bf1 itemmadd1
(integer) 1

# 批量檢查多個元素是否存在於BloomFilter
127.0.0.1:6379&gt; bf.mexists bf1 itemadd1 itemmadd1 itemmadd2
1) (integer) 1
2) (integer) 1
3) (integer) 1

```c

### 2.2.7、BF.INSERT
- 引數
  - BF.INSERT {key} [CAPACITY {cap}] [ERROR {error}] [EXPANSION expansion] [NOCREATE] [NONSCALING] ITEMS {item...}
- 功能
  - 向key指定的Bloom中新增多個元素,新增時可以指定大小和錯誤率,且可以控制在Bloom不存在的時候是否自動建立	
- 時間複雜度
  - O(log N),N是過濾器的層數。
- 引數說明
  - key:布隆過濾器的名字;
  - CAPACITY:[如果過濾器已建立,則此引數將被忽略]。更多的資訊參考<bf.reserve>;
  - ERROR:[如果過濾器已建立,則此引數將被忽略]。更多的資訊參考<bf.reserve>;
  - expansion:布隆過濾器會自動建立一個子過濾器,子過濾器的大小是上一個過濾器大小乘以expansion。expansion的預設值是2,也就是說布隆過濾器擴容預設是2倍擴容。
  - NOCREATE:如果設定了該引數,當布隆過濾器不存在時則不會被建立。用於嚴格區分過濾器的建立和元素插入場景。該引數不能與CAPACITY和ERROR同時設定。
  - NONSCALING:設定此項後,當新增到布隆過濾器中的資料達到初始容量後,不會擴容過濾器,並且會丟擲異常((error) ERR non scaling filter is full)。
  - ITEMS:待插入過濾器的元素列表,該引數必傳。
- 返回值
  - 成功:返回一個數組,陣列的每一個元素可能為1或0,當item一定不存在時陣列元素值為1,當item可能已經存在時陣列元素值為0。
  - 其它情況返回相應的異常資訊。


```c
127.0.0.1:6379&gt; del bfinsert
(integer) 1

127.0.0.1:6379&gt; bf.insert bfinsert CAPACITY 5 ERROR 0.1 EXPANSION 2  NONSCALING ITEMS item1 item2
1) (integer) 1
2) (integer) 1
127.0.0.1:6379&gt; bf.exists bfinsert item5
(integer) 0
127.0.0.1:6379&gt; bf.insert bfinsert CAPACITY 5 ERROR 0.1 EXPANSION 2  NONSCALING ITEMS item1 item2 item3 item4 item5
1) (integer) 0
2) (integer) 0
3) (integer) 1
4) (integer) 1
5) (integer) 0
127.0.0.1:6379&gt; bf.add bfinsert item5
(integer) 0
127.0.0.1:6379&gt; bf.info bfinsert
 1) Capacity
 2) (integer) 5
 3) Size
 4) (integer) 155
 5) Number of filters
 6) (integer) 1
 7) Number of items inserted
 8) (integer) 4
 9) Expansion rate
10) (integer) 2
127.0.0.1:6379&gt; bf.add bfinsert item6
(integer) 1
127.0.0.1:6379&gt; bf.add bfinsert item5
(integer) 0
127.0.0.1:6379&gt; bf.exists bfinsert item5
(integer) 1

# 這裡有個比較有意思的現象,item5未顯示新增成功,但是後續卻顯示exists
# 這說明發生了hash衝突,誤判就是這樣產生的。

2.2.8、BF.SCANDUMP

  • 引數
    • BF.SCANDUMP {key} {iter}
  • 功能
    • 對Bloom進行增量持久化操作(增量儲存);
  • 時間複雜度
    • O(log N),N是過濾器的層數。
  • 引數說明
    • key:布隆過濾器的名字;
    • iter:首次呼叫傳值0,或者上次呼叫此命令返回的結果值;
  • 返回值
    • 返回連續的(iter, data)對,直到(0,NULL),表示DUMP完成。
  • 備註
127.0.0.1:6378&gt; bf.madd bfdump d1 d2 d3 d4 d5 d6 d7
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1
6) (integer) 1
7) (integer) 1
127.0.0.1:6378&gt; bf.scandump bfdump 0
1) (integer) 1
2) "\a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x02\x00\x00\x00\x8a\x00\x00\x00\x00\x00\x00\x00P\x04\x00\x00\x00\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00{\x14\xaeG\xe1zt?\xe9\x86/\xb25\x0e&amp;@\b\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x00"
127.0.0.1:6378&gt; bf.scandump bfdump 1
1) (integer) 139
2) "\x80\x00\b\n\x00$\x00 \b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00\x00\x82$\x04\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\x04\x01@\xa0\x00@\x00\x00\x00\x00\x00\x10@\x00\x02\"\x00 \x00\x00\x04\x00\x00\x00\x00\x00 \x00\x80\x00\x00\"\x04\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00 \x80$\x00 \x00\x00 \x0c$\x00\x00\x00\b`\x00\x00\x00\x00\x00\x00\x00\x00\b\x80\x02 \x04\x00\x00\x00\x00\x00"
127.0.0.1:6378&gt; bf.scandump bfdump 139
1) (integer) 0
2) ""

2.2.9、BF.LOADCHUNK

  • 引數
    • BF.LOADCHUNK {key} {iter} {data}
  • 功能
    • 載入SCANDUMP持久化的Bloom資料;
  • 時間複雜度
    • O(log N),N是過濾器的層數。
  • 引數說明
    • key:目標布隆過濾器的名字;
    • iter:SCANDUMP返回的迭代器的值,和data一一對應;
    • data:SCANDUMP返回的資料塊(data chunk);
  • 返回值
    • 成功則返回OK。
# Python 虛擬碼
# 來源於:https://oss.redislabs.com/redisbloom/Bloom_Commands/

chunks = []
iter = 0
# SCANDUMP
while True:
    iter, data = BF.SCANDUMP(key, iter)
    if iter == 0:
        break
    else:
        chunks.append([iter, data])

# LOADCHUNK
for chunk in chunks:
    iter, data = chunk
    BF.LOADCHUNK(key, iter, data)

2.2.10、BF.INFO

  • 引數
    • BF.INFO {key}
  • 功能
    • 返回BloomFilter的相關資訊;
  • 時間複雜度
    • O(1);
  • 引數說明
    • key:目標布隆過濾器的名字;
  • 返回值
    • Capacity:預設容量;
    • Size:實際佔用情況,但如何計算待進一步確認;
    • Number of filters:過濾器層數;
    • Number of items inserted:已經實際插入的元素數量;
    • Expansion rate:子過濾器擴容係數(預設2);
127.0.0.1:6379&gt; bf.info bf2
 1) Capacity
 2) (integer) 5
 3) Size
 4) (integer) 155
 5) Number of filters
 6) (integer) 1
 7) Number of items inserted
 8) (integer) 5
 9) Expansion rate
10) (integer) 2

2.2.11、BF.DEBUG

  • 引數
    • BF.DEBUG {key}
  • 功能
    • 檢視BloomFilter的內部詳細資訊(如每層的元素個數、錯誤率等);
  • 時間複雜度
    • O(log N),N是過濾器的層數;
  • 引數說明
    • key:目標布隆過濾器的名字;
  • 返回值
    • size:BloomFilter中已插入的元素數量;
    • 每層BloomFilter的詳細資訊
      • bytes:佔用位元組數量;
      • bits:佔用bit位數量,bits = bytes * 8;
      • hashes:該層hash函式數量;
      • hashwidth:hash函式寬度;
      • capacity:該層容量(第一層為BloomFilter初始化時設定的容量,第2層容量 = 第一層容量 * expansion,以此類推);
      • size:該層中已插入的元素數量(各層size之和等於BloomFilter中已插入的元素數量size);
      • ratio:該層錯誤率(第一層的錯誤率 = BloomFilter初始化時設定的錯誤率 * 0.5,第二層為第一層的0.5倍,以此類推,ratio與expansion無關);
# 公眾號 @zxiaofan
# 建立一個容量為5的BloomFilter,其key為“bfexp”;
127.0.0.1:6379&gt; bf.reserve bfexp 0.1 5
OK

# 檢視BloomFilter的內部資訊,此時BloomFilter的層數為1
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:0"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:0 ratio:0.05"

127.0.0.1:6379&gt; bf.madd bfexp 1 2 3 4 5
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:5"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"

127.0.0.1:6379&gt; bf.madd bfexp 11 12 13 14 15
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0
5) (integer) 1

# 新增10個元素後,此時BloomFilter的層數變為2;
# BloomFilter的元素數量為2層過濾器之和(5+4=9),新增“14”時實際因為hash衝突沒新增成功;
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:9"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:4 ratio:0.025"

127.0.0.1:6379&gt; bf.madd bfexp 21 22 23
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:12"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:7 ratio:0.025"
127.0.0.1:6379&gt; bf.madd bfexp  24 25
1) (integer) 1
2) (integer) 1
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:14"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:9 ratio:0.025"
127.0.0.1:6379&gt; bf.madd bfexp 31 32 33 34 35
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1

# 新增20個元素後,此時BloomFilter的層數變為3;
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:19"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:10 ratio:0.025"
4) "bytes:23 bits:184 hashes:7 hashwidth:64 capacity:20 size:4 ratio:0.0125"

3、布隆過濾器的底層原理

3.1、布隆過濾器的底層結構

  布隆過濾器本質是一個巨大的bit陣列(bit array)+幾個不同的無偏hash函式。

  布隆過濾器新增一個item("zxiaofan"),其操作步驟是:

  • 使用多個無偏雜湊函式對item進行hash運算,得到多個hash值hash(zxiaofan);
  • 每個hash值對bit陣列取模得到位陣列中的位置index(zxiaofan);
  • 判斷所有index位是否都為1 ;
  • 位都為1則說明該元素可能已經存在了;
  • 任意一位不為1則說明一定不存在,此時會將不為1的位置為1;

  需要注意的是,雖然使用了無偏hash函式,使得hash值儘可能均勻,但是不同的item計算出的hash值依舊可能重複,所以布隆過濾器返回元素存在,實際是有可能不存在的。

> 取模運算(“Modulus Operation”)和取餘運算(“Remainder Operation ”)兩個概念有重疊的部分但又不完全一致。主要的區別在於對負整數進行除法運算時操作不同。取模主要是用於計算機術語中。取餘則更多是數學概念。a mod b = c,a、b符號一致時,取模、取餘計算得出的C相同;a、b符號不一致時,取模計算的c其符號和b一致,取餘計算的C其符號和a一致。

3.2、最佳hash函式數量與錯誤率的關係

  原始碼中的hash函式數量計算公式:

# hash函式數量計算公式:

# ceil(value):返回不小於value的最小整數;
# log(error):以10為底的對數函式;
# ln(x):以e為底的對數函式;
# ln(2) ≈ 0.693147180559945;
# ln(2)^2 ≈ 0.480453013918201;

bloom-&gt;hashes = (int)ceil(0.693147180559945 * bloom-&gt;bpe);

static double calc_bpe(double error) {
    static const double denom = 0.480453013918201; // ln(2)^2
    double num = log(error);

    double bpe = -(num / denom);
    if (bpe &lt; 0) {
        bpe = -bpe;
    }
    return bpe;
}

  我們通過建立不同錯誤率不同容量的布隆過濾器,整理hash函式數量與錯誤率的關係。

# 公眾號@zxiaofan
# 建立一個key為“bf0.1-2”的布隆過濾器,其錯誤率為0.1,初始容量為100;
127.0.0.1:6379&gt; bf.reserve bf0.1-2 0.1 100
OK
127.0.0.1:6379&gt; bf.reserve bf0.1-3 0.1 1000
OK
127.0.0.1:6379&gt; bf.reserve bf0.01-3 0.01 1000
OK
127.0.0.1:6379&gt; bf.reserve bf0.01-4 0.01 10000
OK
127.0.0.1:6379&gt; bf.reserve bf0.001-4 0.001 10000
OK
127.0.0.1:6379&gt; bf.reserve bf0.001-5 0.001 100000
OK
127.0.0.1:6379&gt; bf.reserve bf0.0001-5 0.0001 100000
OK
127.0.0.1:6379&gt; bf.reserve bf0.00001-5 0.00001 100000
OK
127.0.0.1:6379&gt; bf.reserve bf0.000001-5 0.000001 100000
OK
127.0.0.1:6379&gt; bf.reserve bf0.000001-4 0.000001 10000
OK

# 建立一個key為“bf0.0000001-4”的布隆過濾器,其錯誤率為0.0000001,初始容量為10000;
127.0.0.1:6379&gt; bf.reserve bf0.0000001-4 0.0000001 10000
OK

# 檢視key為“bf0.1-2”的布隆過濾器資訊,hashes表示內部使用的hash函式數量;
127.0.0.1:6379&gt; bf.debug bf0.1-2
1) "size:0"
2) "bytes:78 bits:624 hashes:5 hashwidth:64 capacity:100 size:0 ratio:0.05"

127.0.0.1:6379&gt; bf.debug bf0.1-3
1) "size:0"
2) "bytes:780 bits:6240 hashes:5 hashwidth:64 capacity:1000 size:0 ratio:0.05"

127.0.0.1:6379&gt; bf.debug bf0.01-4
1) "size:0"
2) "bytes:13785 bits:110280 hashes:8 hashwidth:64 capacity:10000 size:0 ratio:0.005"

127.0.0.1:6379&gt; bf.debug bf0.001-5
1) "size:0"
2) "bytes:197754 bits:1582032 hashes:11 hashwidth:64 capacity:100000 size:0 ratio:0.0005"
# 197754 bytes = 197754/1024/1024 ≈ 0.19 M。 

127.0.0.1:6379&gt; bf.debug bf0.0001-5
1) "size:0"
2) "bytes:257661 bits:2061288 hashes:15 hashwidth:64 capacity:100000 size:0 ratio:5e-05"

127.0.0.1:6379&gt; bf.debug bf0.00001-5
1) "size:0"
2) "bytes:317567 bits:2540536 hashes:18 hashwidth:64 capacity:100000 size:0 ratio:5e-06"

127.0.0.1:6379&gt; bf.debug bf0.000001-5
1) "size:0"
2) "bytes:377474 bits:3019792 hashes:21 hashwidth:64 capacity:100000 size:0 ratio:5e-07"

127.0.0.1:6379&gt; bf.debug bf0.000001-4
1) "size:0"
2) "bytes:37748 bits:301984 hashes:21 hashwidth:64 capacity:10000 size:0 ratio:5e-07"

127.0.0.1:6379&gt; bf.debug bf0.0000001-4
1) "size:0"
2) "bytes:43738 bits:349904 hashes:25 hashwidth:64 capacity:10000 size:0 ratio:5e-08"

  由上面的執行結果可以看出,Redis布隆過濾器中最佳hash函式數量與錯誤率的關係如下:

錯誤率{error_rate}hash函式的最佳數量
0.1 5
0.01 8
0.001 11
0.0001 15
0.00001 18
0.000001 21
0.0000001 25

3.3、所需儲存空間與錯誤率及容量關係

  通過建立不同錯誤率不同容量的布隆過濾器,整理儲存空間與錯誤率及容量的關係。

127.0.0.1:6379&gt; bf.reserve bf0.0001-6 0.0001 1000000
OK
127.0.0.1:6379&gt; bf.reserve bf0.0001-7 0.0001 10000000
OK
127.0.0.1:6379&gt; bf.reserve bf0.0001-8 0.0001 100000000
OK

127.0.0.1:6379&gt; bf.debug bf0.0001-6
1) "size:0"
2) "bytes:2576602 bits:20612816 hashes:15 hashwidth:64 capacity:1000000 size:0 ratio:5e-05"

127.0.0.1:6379&gt; bf.debug bf0.0001-7
1) "size:0"
2) "bytes:25766015 bits:206128120 hashes:15 hashwidth:64 capacity:10000000 size:0 ratio:5e-05"

127.0.0.1:6379&gt; bf.debug bf0.0001-8
1) "size:0"
2) "bytes:257660148 bits:2061281184 hashes:15 hashwidth:64 capacity:100000000 size:0 ratio:5e-05"
# 257660148 bytes = 257660148/1024/1024 ≈ 245.7 M。 
錯誤率{error_rate}元素數量{capacity}佔用記憶體(單位M)
0.001 10萬 0.19
0.001 1百萬 1.89
0.001 1千萬 18.9
0.001 1億 188.6
0.0001 10萬 0.25
0.0001 1百萬 2.5
0.0001 1千萬 24.6
0.0001 1億 245.7
0.00001 10萬 0.3
0.00001 1百萬 3.01
0.00001 1千萬 30.1
0.00001 1億 302.9

  佔用記憶體(單位M) = bytes值/1024/1024。

  從上述對比分析可以看出,錯誤率{error_rate}越小,所需的儲存空間越大; 初始化設定的元素數量{capacity}越大,所需的儲存空間越大,當然如果實際遠多於預設時,準確率就會降低。

  在1千萬資料場景下,error_rate為0.001、0.0001、0.00001實際佔用記憶體都是30M以下,此時如果對準確性要求高,初始化時將錯誤率設定低一點是完全無傷大雅的。

  RedisBloom官方預設的error_rate是 0.01,預設的capacity是 100,原始碼如下:

// RedisBloom/src/rebloom.c

static double BFDefaultErrorRate = 0.01;
static size_t BFDefaultInitCapacity = 100;

3.4、布隆過濾器如何擴容

  先執行幾行命令,看看實際效果。

# 公眾號 @zxiaofan
# 建立一個容量為5的BloomFilter,其key為“bfexp”;
127.0.0.1:6379&gt; bf.reserve bfexp 0.1 5
OK

# 檢視BloomFilter的內部資訊,此時BloomFilter的層數為1
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:0"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:0 ratio:0.05"

127.0.0.1:6379&gt; bf.madd bfexp 1 2 3 4 5
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:5"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"

127.0.0.1:6379&gt; bf.madd bfexp 11 12 13 14 15
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0
5) (integer) 1

# 新增10個元素後,此時BloomFilter的層數變為2;
# BloomFilter的元素數量為2層過濾器之和(5+4=9),新增“14”時實際因為hash衝突沒新增成功;
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:9"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:4 ratio:0.025"

127.0.0.1:6379&gt; bf.madd bfexp 21 22 23
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:12"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:7 ratio:0.025"
127.0.0.1:6379&gt; bf.madd bfexp  24 25
1) (integer) 1
2) (integer) 1

# 新增14個元素後,還未達到BloomFilter擴容閾值,層數依舊為2;
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:14"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:9 ratio:0.025"

127.0.0.1:6379&gt; bf.madd bfexp 31 32 33 34 35
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1

# 新增20個元素後,此時BloomFilter的層數變為3;
127.0.0.1:6379&gt; bf.debug bfexp
1) "size:19"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:10 ratio:0.025"
4) "bytes:23 bits:184 hashes:7 hashwidth:64 capacity:20 size:4 ratio:0.0125"

BloomFilter擴容邏輯:

  • 插入m個元素,計算實際插入BloomFilter的元素數量;
  • 如果實際插入元素數量 > BloomFilter的容量,則觸發擴容;
  • 擴容的倍數為BloomFilter初始化時設定的expansion(預設2);

BloomFilter擴容注意事項:

  • 擴容觸發的條件是 實際插入 > 容量,實際插入數量 = 容量時,是不會觸發擴容的;
  • 實際插入指的是插入成功,即使計劃插入的資料過濾器中沒有,但由於hash衝突匯入插入失敗,這種也不算實際插入成功。假設容量是20,如果插入21個元素,但由於重複甚至於hash衝突,導致實際插入的數量不足21個,此時也不會觸發擴容;

4、布隆過濾器有哪些應用場景

4.1、郵件黑名單&網站黑名單

  郵箱地址數十億計且長度不固定,我們需要從海量的郵箱地址中識別出垃圾郵箱地址。當一個郵箱地址被判定為垃圾郵箱後,就將此地址新增進布隆過濾器中即可。

  同理,全球資訊網上的URL地址中包含了大量的非法或惡意URL,利用布隆過濾器也可以快速判斷此類URL。當布隆過濾器返回結果為存在時,才對URL進行進一步判定處理。

4.2、新聞推薦去重

  對於百度新聞、頭條新聞等資訊推薦平臺,為了儘可能提升使用者體驗,應最大可能保證推薦給使用者的新聞不重複,將已推薦給使用者的文章ID存入布隆過濾器,再次推薦時先判斷是否已推送即可。

4.3、快取穿透&惡意攻擊

  快取穿透:是指查詢了快取和資料庫中都沒有的資料。當此類查詢請求量過大時(比如系統被惡意攻擊),快取系統或資料庫的壓力將增大,極容易宕機。

  方式1:當查詢DB中發現某資料不存在時,則將此資料ID存入布隆過濾器,每次查詢時先判斷是否存在於布隆過濾器,存在則說明資料庫無此資料,無需繼續查詢了。當然此種方式僅能處理同一個ID重複訪問的場景。

  方式2:如果攻擊者惡意構造了大量不重複的且資料庫中不存在的資料呢,此時可將資料庫中已有的資料的唯一ID放入布隆過濾器,每次查詢時先判斷是否存在於布隆過濾器,存在才呼叫後端系統查詢,則可有效過濾惡意攻擊。

  使用方式1需要防止指定ID最初不存在於DB中,遂將此ID存入“資料不存在的過濾器”中,但後續DB又新增了此ID,因為布隆過濾器不支援刪除操作,一旦發生此類場景,就肯定會出現誤判了。

  使用方式2需要注意資料的增量,避免資料庫中新增了資料而過濾器中還沒有導致無法查詢到資料。當然如果此時DB中刪除了指定資料,布隆過濾器是無法隨之刪除指紋標記的。

  瞭解了原理方能如臂使指。此外建議,生產資料的ID應定義生成規則及校驗規則(比如身份證的最後一位就是校驗位),這樣每次查詢先判斷這個ID是否有效,有效才進行後續的步驟,這樣可以充分過濾外部的惡意攻擊。

4.4、網頁爬蟲URL去重

  網路爬蟲是一個自動提取網頁的程式,它為搜尋引擎從全球資訊網上下載網頁,是搜尋引擎的重要組成。傳統爬蟲從一個或若干初始網頁的URL開始,獲得初始網頁上的URL,在抓取網頁的過程中,不斷從當前頁面上抽取新的URL放入佇列,直到滿足系統的一定停止條件。由於網站之間存在互相引用,抓取的URL可能存在重複,為了避免爬取重複的資料,可以將已爬取的URL放入布隆過濾器中,每次爬取新URL時先做判斷。

4.5、查詢加速

  Google BigTable、Apache HBase、Apache Cassandra、Postgresql 等Key-Value儲存系統,使用布隆過濾器確定資料是否存在,從而減少代價相對較高的磁碟查詢。

  在HBase中,一個HFile一旦被寫完就只會被查詢不會被更新。將檔案的所有key進行計算,生成這個檔案的布隆過濾器,並將其寫入到元資料中,以後所有對該檔案的查詢都會先查詢對應的布隆過濾器,如果在布隆過濾器中不存在則不需要訪問該檔案,節省了大量的對磁碟的低速訪問。

  Cassandra原理類似,採用了追加而不是修改的方式來處理資料檔案。一塊完整的資料被dump到檔案後就不會再被更新。在每個檔案被dump到硬碟上時,都會對該檔案生成一個布隆過濾器,而該布隆過濾器會被存放到記憶體中。所有對該檔案的訪問都會先訪問對應的布隆過濾器,如果布隆過濾器返回不存在則無需訪問硬碟上的檔案。從而大大提高了查詢的效率。

4.6、防止重複請求

  第一次請求,將請求引數放入布隆過濾器中,第二次請求時,先判斷請求引數是否存在於BloomFilter中。

4.7、區塊鏈應用

  區塊鏈中使用布隆過濾器來加快錢包同步;以太坊使用布隆過濾器用於快速查詢以太坊區塊鏈的日誌。

  比特幣錢包如何知道有多少錢(比特幣錢包如何知道有多少UTXO),比特幣系統沒有餘額的概念,它使用的是UTXO模型(Unspent Transaction Outputs,未使用過的交易輸出)。比特幣每一筆交易記錄了時間、傳送人、接收人和金額。那如果要計算A的餘額,那麼就要遍歷所有跟A有關的交易,減去A傳送的每一筆金額,並加上A接收的每一筆金額。

  輕客戶端下載完整的區塊鏈賬本自己查詢,這顯然是不現實的,如果輕客戶端告訴全節點自己的錢包地址,則又洩漏了隱私。現有的實現方式是,錢包節點以布隆過濾器的方式告訴全節點自己的錢包地址,全節點返回可能相關的UTXO。

  以太坊記錄交易日誌也採用了布隆過濾器,以太坊的每個區塊頭包含當前區塊中所有收據的日誌的布隆過濾器logsBloom,便於高效查詢日誌資料。

  數學改變生活。

5、布隆過濾器的優缺點

5.1、布隆過濾器的優勢

  • 【適合大資料場景】:支援海量資料場景下高效判斷元素是否存在;
  • 【節省空間】:不儲存資料本身,僅儲存hash結果取模運算後的位標記;
  • 【資料保密】:不儲存資料本身,適合某些保密場景;

5.2、布隆過濾器的缺點

  • 【誤判】:由於存在hash碰撞,匹配結果如果是“存在於過濾器中”,實際不一定存在;
  • 【不可刪除】:沒有儲存元素本身,所以只能新增但不可刪除;
  • 【空間利用率不高】:建立過濾器時需提前預估建立,當錯誤率越低時,為了儘可能避免hash碰撞,冗餘的空間就越多;需要注意的是,空間利用率不高和節省空間並不衝突;
  • 【容量滿時誤報率增加】當容量快滿時,hash碰撞的概率變大,插入、查詢的錯誤率也就隨之增加了。

5.3、布隆過濾器其他問題

  • 【不支援計數】:同一個元素可以多次插入,但效果和插入一次相同;
  • 【查詢速度受錯誤率影響】:由於錯誤率影響hash函式的數量,當hash函式越多,每次插入、查詢需做的hash操作就越多;

6、延伸拓展

6.1、超大規模布隆過濾器如何處理

  除自建Redis外,阿里雲-雲資料庫Redis是又一不錯的選擇,即買即用。但需要注意的是,阿里雲的社群版主從版Redis單機支援10W QPS,如果資料量過大,需要遷移到叢集版;4096GB叢集效能增強版最大支援6KW QPS。

  面對超大規模資料,除了使用更大規格的叢集版Redis,我們是否還有其他解決方式呢?結合前人的優秀思路(Oracle大型機轉為分散式MySQL叢集),拆分key也一個不錯的思路,即讓key均勻分散到不同的小叢集中。

  回到我們的問題,如果我們需要校驗的資料量超大,比如搜尋引擎的爬蟲需要判重URL,使用一個布隆過濾器效能肯定受影響。那麼我們可以 取Hash(URL)的前幾位 作為不同布隆過濾器的標記,此時URL就將均勻的分佈到不同的布隆過濾器中。

【玩轉Redis系列文章 近期精選 @zxiaofan】
《玩轉Redis-HyperLogLog原理探索》

《玩轉Redis-HyperLogLog統計微博日活月活》

《玩轉Redis-京東簽到領京豆如何實現》

《玩轉Redis-老闆帶你深入理解分散式鎖》

《玩轉Redis-如何高效訪問Redis中的海量資料》


>祝君好運!

推薦:山東濱州屬於哪個市