❶ 布隆過濾器
布隆過濾器 (英語:Bloom Filter)是 1970 年由布隆提出的。它實際上是一個很長的二進制向量和一系列隨機映射函數。主要用於判斷一個元素是否在一個集合中。
通常我們會遇到很多要判斷一個元素是否在某個集合中的業務場景,一般想到的是將集合中所有元素保存起來,然後通過比較確定。鏈表、樹、散列表(又叫哈希表,Hash table)等等數據結構都是這種思路。但是隨著集合中元素的增加,我們需要的存儲空間也會呈現線性增長,最終達到瓶頸。同時檢索速度也越來越慢,上述三種結構的檢索時間復雜度分別為,,。
這個時候,布隆過濾器(Bloom Filter)就應運而生。
了解布隆過濾器原理之前,先回顧下 Hash 函數原理。
哈希函數的概念是:將任意大小的輸入數據轉換成特定大小的輸出數據的函數,轉換後的數據稱為哈希值或哈希編碼,也叫散列值。下面是一幅示意圖:
所有散列函數都有如下基本特性:
但是用 hash表存儲大數據量時,空間效率還是很低,當只有一個 hash 函數時,還很容易發生哈希碰撞。
BloomFilter 是由一個固定大小的二進制向量或者點陣圖(bitmap)和一系列映射函數組成的。
在初始狀態時,對於長度為 m 的位數組,它的所有位都被置為0,如下圖所示:
[圖片上傳失敗...(image-303c04-1595324887187)]
當有變數被加入集合時,通過 K 個映射函數將這個變數映射成點陣圖中的 K 個點,把它們置為 1(假定有兩個變數都通過 3 個映射函數)。
查詢某個變數的時候我們只要看看這些點是不是都是 1 就可以大概率知道集合中有沒有它了
為什麼說是可能存在,而不是一定存在呢?那是因為映射函數本身就是散列函數,散列函數是會有碰撞的。
布隆過濾器的誤判是指多個輸入經過哈希之後在相同的bit位置1了,這樣就無法判斷究竟是哪個輸入產生的,因此誤判的根源在於相同的 bit 位被多次映射且置 1。
這種情況也造成了布隆過濾器的刪除問題,因為布隆過濾器的每一個 bit 並不是獨占的,很有可能多個元素共享了某一位。如果我們直接刪除這一位的話,會影響其他的元素。(比如上圖中的第 3 位)
相比於其它的數據結構,布隆過濾器在空間和時間方面都有巨大的優勢。布隆過濾器存儲空間和插入/查詢時間都是常數 ,另外,散列函數相互之間沒有關系,方便由硬體並行實現。布隆過濾器不需要存儲元素本身,在某些對保密要求非常嚴格的場合有優勢。
布隆過濾器可以表示全集,其它任何數據結構都不能;
但是布隆過濾器的缺點和優點一樣明顯。誤算率是其中之一。隨著存入的元素數量增加,誤算率隨之增加。但是如果元素數量太少,則使用散列表足矣。
另外,一般情況下不能從布隆過濾器中刪除元素。我們很容易想到把位數組變成整數數組,每插入一個元素相應的計數器加 1, 這樣刪除元素時將計數器減掉就可以了。然而要保證安全地刪除元素並非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器裡面。這一點單憑這個過濾器是無法保證的。另外計數器回繞也會造成問題。
在降低誤算率方面,有不少工作,使得出現了很多布隆過濾器的變種。
在程序的世界中,布隆過濾器是程序員的一把利器,利用它可以快速地解決項目中一些比較棘手的問題。
如網頁 URL 去重、垃圾郵件識別、大集合中重復元素的判斷和緩存穿透等問題。
布隆過濾器的典型應用有:
知道了布隆過濾器的原理和使用場景,我們可以自己實現一個簡單的布隆過濾器
分布式環境中,布隆過濾器肯定還需要考慮是可以共享的資源,這時候我們會想到 Redis,是的,Redis 也實現了布隆過濾器。
當然我們也可以把布隆過濾器通過 bloomFilter.writeTo() 寫入一個文件,放入OSS、S3這類對象存儲中。
Redis 提供的 bitMap 可以實現布隆過濾器,但是需要自己設計映射函數和一些細節,這和我們自定義沒啥區別。
Redis 官方提供的布隆過濾器到了 Redis 4.0 提供了插件功能之後才正式登場。布隆過濾器作為一個插件載入到 Redis Server 中,給 Redis 提供了強大的布隆去重功能。
在已安裝 Redis 的前提下,安裝 RedisBloom,有兩種方式
直接編譯進行安裝
使用Docker進行安裝
使用
布隆過濾器基本指令:
我們只有這幾個參數,肯定不會有誤判,當元素逐漸增多時,就會有一定的誤判了,這里就不做這個實驗了。
上面使用的布隆過濾器只是默認參數的布隆過濾器,它在我們第一次 add 的時候自動創建。
Redis 還提供了自定義參數的布隆過濾器,bf.reserve 過濾器名 error_rate initial_size
但是這個操作需要在 add 之前顯式創建。如果對應的 key 已經存在,bf.reserve 會報錯
我是一名 Javaer,肯定還要用 Java 來實現的,Java 的 Redis 客戶端比較多,有些還沒有提供指令擴展機制,筆者已知的 Redisson 和 lettuce 是可以使用布隆過濾器的,我們這里用 Redisson
為了解決布隆過濾器不能刪除元素的問題,布穀鳥過濾器橫空出世。論文《Cuckoo Filter:Better Than Bloom》作者將布穀鳥過濾器和布隆過濾器進行了深入的對比。相比布穀鳥過濾器而言布隆過濾器有以下不足:查詢性能弱、空間利用效率低、不支持反向操作(刪除)以及不支持計數。
❷ 布隆過濾器(Bloom Filter)的原理和實現
看下下面幾個問題
通常的做法有如下幾種思路:
在繼續介紹布隆過濾器的原理時,先講解下關於哈希函數的預備知識。
可以明顯的看到,原始數據經過哈希函數的映射後稱為了一個個的哈希編碼,數據得到壓縮。哈希函數是實現哈希表和布隆過濾器的基礎。
布隆過濾器(Bloom Filter)的核心實現是一個超大的位數組和幾個哈希函數。假設位數組的長度為m,哈希函數的個數為k
❸ 藉助Redis Bitmap實現簡單的布隆過濾器
在之前的 一篇文章 中,我們已經深入理解了布隆過濾器的基本原理,並且了解到它在緩存系統中有較多的應用。Redis提供的Bitmap正好能夠作為布隆過濾器所需要的位數組的基礎,本文先簡要介紹Bitmap,然後給出基於它的布隆過濾器實現。
Bitmap在Redis中並不是一個單獨的數據類型,而是由字元串類型(Redis內部稱Simple Dynamic String,SDS)之上定義的與比特相關的操作實現的,此時SDS就被當做位數組了。下面是在redis-cli中使用getbit和setbit指令的操作示例。
Redis的Bitmap是自動擴容的,亦即get/set到高位時,就會主動填充0。此外,還有bitcount指令用於計算特定位元組范圍內1的個數,bitop指令用來執行位運算(支持and、or、xor和not)。相應的用法可以查詢Redis官方文檔等。
下面我們基於Redis(Codis)實現布隆過濾器RedisBloomFilter。根據之前講解布隆過濾器的文章,要初始化一個布隆過濾器的話,需要兩個參數:預估的元素數量,以及可接受的最大誤差(即假陽性率)。另外,我們也需要傳入Jodis的連接池實例JedisResourcePool,以方便在Redis上操作。RedisBloomFilter類的成員和構造方法如下所示。
為了區分出布隆過濾器對應的Key,在原始Key的前面都加上"bf:"前綴。Bitmap長度bitmapLength和哈希函數個數numHashFunctions則利用Guava版實現中的方法來計算。
然後,我們需要計算一個元素被k個哈希函數散列後,對應到Bitmap的哪些比特上。這里仍然借鑒了Guava的BloomFilterStrategies實現,採用MurmurHash和雙重哈希進行散列。為了應用簡單,假設所有元素固定為字元串類型,不用泛型。
然後我們就可以通過Jedis的setbit()和getbit()方法來實現向布隆過濾器中插入元素與查詢元素是否存在的邏輯了。一個元素會對應多個比特,為了提高效率,流水線就派上用場了。另外,setbit指令不會重置對應Key的過期時間戳。
完畢,寫個簡單的單元測試吧。假設現在用布隆過濾器來存儲已讀帖子的ID,Key中包含用戶ID和時間。每天預估的每用戶最大閱讀量是3000篇,最大誤差3%。
觀察輸出。