《玩轉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> bf.add bf1 itemadd1
(integer) 1
# 向BloomFilter批量添加多個元素
127.0.0.1:6379> bf.madd bf1 itemmadd1 itemmadd2
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> bf.exists itemmadd1
(error) ERR wrong number of arguments for 'bf.exists' command
127.0.0.1:6379> bf.exists bf1 itemmadd1
(integer) 1
# 批量檢查多個元素是否存在於BloomFilter
127.0.0.1:6379> 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> del bfinsert
(integer) 1
127.0.0.1:6379> 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> bf.exists bfinsert item5
(integer) 0
127.0.0.1:6379> 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> bf.add bfinsert item5
(integer) 0
127.0.0.1:6379> 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> bf.add bfinsert item6
(integer) 1
127.0.0.1:6379> bf.add bfinsert item5
(integer) 0
127.0.0.1:6379> 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> 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> 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&@\b\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x00"
127.0.0.1:6378> 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> 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> 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> bf.reserve bfexp 0.1 5
OK
# 查看BloomFilter的內部信息,此時BloomFilter的層數爲1
127.0.0.1:6379> 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> 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> 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> 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> 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> bf.madd bfexp 21 22 23
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> 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> bf.madd bfexp 24 25
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> 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> 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> 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->hashes = (int)ceil(0.693147180559945 * bloom->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 < 0) {
bpe = -bpe;
}
return bpe;
}
我們通過創建不同錯誤率不同容量的布隆過濾器,整理hash函數數量與錯誤率的關係。
# 公衆號@zxiaofan
# 創建一個key爲“bf0.1-2”的布隆過濾器,其錯誤率爲0.1,初始容量爲100;
127.0.0.1:6379> bf.reserve bf0.1-2 0.1 100
OK
127.0.0.1:6379> bf.reserve bf0.1-3 0.1 1000
OK
127.0.0.1:6379> bf.reserve bf0.01-3 0.01 1000
OK
127.0.0.1:6379> bf.reserve bf0.01-4 0.01 10000
OK
127.0.0.1:6379> bf.reserve bf0.001-4 0.001 10000
OK
127.0.0.1:6379> bf.reserve bf0.001-5 0.001 100000
OK
127.0.0.1:6379> bf.reserve bf0.0001-5 0.0001 100000
OK
127.0.0.1:6379> bf.reserve bf0.00001-5 0.00001 100000
OK
127.0.0.1:6379> bf.reserve bf0.000001-5 0.000001 100000
OK
127.0.0.1:6379> bf.reserve bf0.000001-4 0.000001 10000
OK
# 創建一個key爲“bf0.0000001-4”的布隆過濾器,其錯誤率爲0.0000001,初始容量爲10000;
127.0.0.1:6379> bf.reserve bf0.0000001-4 0.0000001 10000
OK
# 查看key爲“bf0.1-2”的布隆過濾器信息,hashes表示內部使用的hash函數數量;
127.0.0.1:6379> 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> 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> 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> 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> 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> 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> 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> 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> 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> bf.reserve bf0.0001-6 0.0001 1000000
OK
127.0.0.1:6379> bf.reserve bf0.0001-7 0.0001 10000000
OK
127.0.0.1:6379> bf.reserve bf0.0001-8 0.0001 100000000
OK
127.0.0.1:6379> 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> 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> 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> bf.reserve bfexp 0.1 5
OK
# 查看BloomFilter的內部信息,此時BloomFilter的層數爲1
127.0.0.1:6379> 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> 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> 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> 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> 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> bf.madd bfexp 21 22 23
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> 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> bf.madd bfexp 24 25
1) (integer) 1
2) (integer) 1
# 添加14個元素後,還未達到BloomFilter擴容閾值,層數依舊爲2;
127.0.0.1:6379> 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> 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> 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原理探索》