1. 程式人生 > 實用技巧 >15. 使用布隆過濾器從海量資料中查詢一個值是否存在

15. 使用布隆過濾器從海量資料中查詢一個值是否存在

楔子

我們前面介紹過 HyperLogLog 可以用來做基數統計,但它沒提供判斷一個值是否存在的查詢方法,那我們如何才能在海量資料之中判斷一個值是否存在呢?

因為是海量資料,所以我們就無法將每個鍵值都存起來,然後再從結果中檢索資料了,比如資料庫中的 select count(1) from tablename where id='XXX',或者是使用 Redis 普通的查詢方法 get XXX 等方式,這樣是不合適的。我們只能依靠專門處理此問題的特殊方法來實現資料的查詢。

而完成一個功能的就是我們今天的主角:布隆過濾器。

布隆過濾器

什麼是布隆過濾器

布隆過濾器(Bloom Filter)是 1970 年由布隆提出的。它實際上是一個很長的二進位制向量和一系列隨機對映函式。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都比一般的演算法要好的多,缺點是有一定的誤識別率和刪除困難。也就是說布隆過濾器的優點就是計算和查詢速度很快,但是缺點也很明顯,就是存在一定的誤差。

但是在 Redis 中不能直接使用布隆過濾器,但我們可以通過 Redis 4.0 版本之後提供的 modules(擴充套件模組)的方式引入,本文提供兩種方式的開啟方式。

方式一:編譯方式

1. 下載並安裝布隆過濾器

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

編譯正常執行完,會在根目錄生成一個 redisbloom.so 檔案。

2. 啟動 Redis 伺服器

./redis-server redis.conf --loadmodule redisbloom.so檔案路徑

其中 --loadmodule 為載入擴充套件模組的意思,後面跟的是 redisbloom.so 檔案的路徑。

方式二:docker方式

也是本文所使用的方式,直接拉取映象然後執行即可。

docker pull redislabs/rebloom  # 拉取映象
docker run -p 6379:6379 redislabs/rebloom  # 執行容器

布隆過濾器的使用

關於布隆過濾器的原理我們後面會說,目前的話先來看看使用。布隆過濾器的命令不是很多,主要包含以下幾個:

  • bf.add:新增元素
  • bf.exists:判斷某個元素是否存在
  • bf.madd:新增多個元素
  • bf.mexists:判斷多個元素是否存在
  • bf.reserve:設定布隆過濾器的準確率

舉慄說明:

新增元素:

127.0.0.1:6379> bf.add user mea
(integer) 1
127.0.0.1:6379> bf.add user kano
(integer) 1
127.0.0.1:6379> bf.add user nana
(integer) 1
127.0.0.1:6379> 

判斷元素是否存在:

127.0.0.1:6379> bf.exists user mea
(integer) 1
127.0.0.1:6379> bf.exists user mea1
(integer) 0
127.0.0.1:6379>

新增多個元素:

127.0.0.1:6379> bf.madd vtb mea kano nana
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379>

判斷多個元素是否存在:

127.0.0.1:6379> bf.mexists user mea kano kano1
1) (integer) 1
2) (integer) 1
3) (integer) 0
127.0.0.1:6379> 

可以看出以上結果沒有任何誤差,然後還有一個布隆過濾器的準確率,不過在介紹它之前,我們先使用Python操作一下Redis的布隆過濾器。

我們之前使用Python操作Redis使用第三方模組也叫redis,但是對於布隆過濾器來說,這個模組是不支援的,我們需要使用另一個第三方模組:redisbloom,直接pip install redisbloom安裝即可。

其實redisbloom底層還是使用了我們之前的redis模組。

from redisbloom.client import Client

# 我們之前建立連線使用的是redis.Redis,而這個Client繼承自Redis
# 所以我們之前的一些操作,這裡都可以用
client = Client(host="47.94.174.89", decode_responses="utf-8")

# 新增單個元素
client.bfAdd("VTuber", "mea")

# 新增多個元素
client.bfMAdd("VTuber", "kano", "nana")

# 判斷單個元素是否存在
print(client.bfExists("VTuber", "kano"))  # 1

# 判斷多個元素是否存在
print(client.bfMExists("VTuber", "kano", "nana", "mea", "xxx"))  # [1, 1, 1, 0]

設定布隆過濾器的準確率:

127.0.0.1:6379> bf.reserve user 0.01 200
(error) ERR item exists  # 對於一個已經存在的key,不可以使用bf.reserve
127.0.0.1:6379> bf.reserve user1 0.01 200  
OK
127.0.0.1:6379>

key後面的兩個值分別表示:error_rate、initial_size

  • error_rate:允許布隆過濾器的錯誤率,這個值越低過濾器佔用空間也就越大,以為此值決定了位陣列的大小,位陣列是用來儲存結果的,它的空間佔用的越大 (儲存的資訊越多),錯誤率就越低,它的預設值是 0.01;
  • initial_size:布隆過濾器儲存的元素大小,實際儲存的值大於此值,準確率就會降低,它的預設值是 100。

布隆過濾器常見使用場景有:

  • 垃圾郵件過濾;
  • 爬蟲裡的 URL 去重;
  • 判斷一個值在億級資料中是否存在。

布隆過濾器在資料庫領域的使用也比較廣泛,例如:HBase、Cassandra、LevelDB、RocksDB 內部都有使用布隆過濾器。

對於布隆過濾器而言,資料量增大到一定程度之後誤差也會隨之增大。

原理解析

Redis 布隆過濾器的實現,依靠的是它資料結構中的一個位數組,每次儲存鍵值的時候,不是直接把資料儲存在資料結構中,因為這樣太佔空間了,它是利用幾個不同的無偏雜湊函式,把此元素的 hash 值均勻的儲存在位陣列中,也就是說,每次新增時會通過幾個無偏雜湊函式算出它的一些位置,把這些位置設定成 1 就完成了新增操作。

當進行元素判斷時,查詢此元素的幾個雜湊位置上的值是否為 1,如果全部為 1,則表示此值存在,如果有一個值為 0,則表示不存在。因為此位置是通過 hash 計算得來的,所以即使這個位置是 1,並不能確定是一定是該元素把它標識為 1 的,因此布隆過濾器查詢此值存在時,此值不一定存在,但查詢此值不存在時,此值一定不存在。

並且當位陣列儲存值比較稀疏的時候,查詢的準確率越高,而當位陣列儲存的值越來越多時,誤差也會增大。

位陣列和 key 之間的關係,如下圖所示:

小結

通過本文我們知道可以使用 Redis 4.0 之後提供的 modules 方式來開啟布隆過濾器,並學習了布隆過濾器的三個重要操作方法 bf.add 新增元素、bf.exists 查詢元素是否存在,還有 bf.reserve 設定布隆過濾器的準確率,其中 bf.reserve 有 2 個重要的引數:錯誤率和陣列大小,錯誤率設定的越低,陣列設定的越大,需要儲存的空間就越大,相對來說查詢的錯誤率也越低,需要如何設定需要使用者根據實際情況進行調整。我們也知道布隆過濾器的特點:當它查詢有資料時,此資料不一定真的存在,當它查詢沒有此資料時,此資料一定不存在。