前言
-
文章首發於微信公衆號大白話布隆過濾器,又能和面試官扯皮了~
-
近期在做推薦系統中已讀內容去重的相關內容,剛好用到了布隆過濾器,於是寫了一篇文章記錄分享一下。
-
文章的篇幅不是很長,主要講了布隆過濾器的核心思想,目錄如下:
什麼是布隆過濾器?
-
布隆過濾器是由一個長度爲
m
比特的位數組與k
個哈希函數組成的數據結構。比特數組均初始化爲0
,所有哈希函數都可以分別把輸入數據儘量均勻地散列。 -
當插入一個元素時,將其數據通過
k
個哈希函數轉換成k
個哈希值,這k
個哈希值將作爲比特數組的下標
,並將數組中的對應下標的值置爲1
。 -
當查詢一個元素時,同樣會將其數據通過
k
個哈希函數轉換成k
個哈希值(數組下標),查詢數組中對應下標的值,如果有一個下標的值爲0
表明該元素一定不在集合中,如果全部下標的值都爲1
,表明該元素有可能
在集合中。至於爲什麼有可能在集合中? 因爲有可能某個或者多個下標的值爲 1 是受到其他元素的影響,這就是所謂的假陽性
,下文會詳細講述。 -
無法刪除一個元素,爲什麼呢?因爲你刪除的元素的哈希值可能和集合中的某個元素的哈希值有相同的,一旦刪除了這個元素會導致其他的元素也被刪除。
-
下圖示出一個
m=18
,k=3
的布隆過濾器示例。集合中的 x、y、z 三個元素通過 3 個不同的哈希函數散列到位數組中。當查詢元素 w 時,因爲有一個比特爲 0,因此 w 不在該集合中。
假陽性概率的計算
-
假陽性是布隆過濾器的一個痛點,因此需要不擇一切手段來使假陽性的概率降低,此時就需要計算一下假陽性的概率了。
-
假設我們的哈希函數選擇位數組中的比特時,都是等概率的。當然在設計哈希函數時,也應該儘量滿足均勻分佈。
-
在位數組長度
m
的布隆過濾器中插入一個元素,它的其中一個哈希函數會將某個特定的比特置爲1
。因此,在插入元素後,該比特仍然爲 0 的概率是: -
現有
k
個哈希函數,並插入n
個元素,自然就可以得到該比特仍然爲 0 的概率是: -
反過來講,它已經被置爲
1
的概率就是: -
也就是說,如果在插入
n
個元素後,我們用一個不在集合中的元素來檢測,那麼被誤報爲存在於集合中的概率(也就是所有哈希函數對應的比特都爲1
的概率)爲: -
當
n
比較大時,根據極限公式,可以近似得出假陽性率: - 所以,在哈希函數個數
k
一定的情況下有如下結論:-
位數組長度 m 越大,假陽性率越低。
-
已插入元素的個數 n 越大,假陽性率越高。
-
優點
-
用比特數組表示,不用存儲數據本身,對空間的節省相比於傳統方式佔據絕對的優勢。
-
時間效率很高,無論是插入還是查詢,只需要簡單的經過哈希函數轉換,時間複雜度均爲
O(k)
。
缺點
-
存在
假陽性
的概率,準確率要求高的場景不太適用。 -
只能插入和查詢,不能刪除了元素。
應用場景
-
布隆過濾器的用途很多,但是主要的作用就是去重,這裏列舉幾個使用場景。
爬蟲重複 URL 檢測
-
試想一下,百度是一個爬蟲,它會定時蒐集各大網站的信息,文章,那麼它是如何保證爬取到文章信息不重複,它會將 URL 存放到布隆過濾器中,每次爬取之前先從布隆過濾器中判斷這個 URL 是否存在,這樣就避免了重複爬取。當然這種存在假陽性的可能,但是隻要你的比特數組足夠大,
假陽性
的概率會很低,另一方面,你認爲百度會在意這種的誤差嗎,你的一篇文章可能因爲假陽性概率沒有收錄到,對百度有影響嗎?
抖音推薦功能
-
讀者朋友們應該沒人沒刷過抖音吧,每次刷的時候抖音給你的視頻有重複的嗎?他是如何保證推薦的內容不重複的呢?
-
最容易想到的就是抖音會記錄用戶的歷史觀看記錄,然後從歷史記錄中排除。這是一種解決辦法,但是性能呢?不用多說了,有點常識的都知道這不可能。
-
解決這種重複的問題,布隆過濾器有着絕對的優勢,能夠很輕鬆的解決。
防止緩存穿透
-
緩存穿透是指查詢一條數據庫和緩存都沒有的一條數據,就會一直查詢數據庫,對數據庫的訪問壓力會一直增大。
-
布隆過濾器在解決緩存穿透的問題效果也是很好,這裏不再細說,後續文章會寫。
如何實現布隆過濾器?
-
瞭解布隆過濾器的設計思想之後,想要實現一個布隆過濾器其實很簡單,陳某這裏就不再搬門弄斧了,介紹一下現成的實現方式吧。
Redis 實現
-
Redis4.0 之後推出了插件的功能,下面用 docker 安裝:
docker pull redislabs/rebloom
docker run -p6379:6379 redislabs/rebloom
-
安裝完成後連接 redis 即可,運行命令:
redis-cli
-
至於具體的使用這裏不再演示了,直接看官方文檔和教程,使用起來還是很簡單的。
Guava 實現
-
guava 對應布隆過濾器的實現做出了支持,使用 guava 可以很輕鬆的實現一個布隆過濾器。
1. 創建布隆過濾器
-
創建布隆過濾器,如下:
BloomFilter<Integer> filter = BloomFilter.create(
Funnels.integerFunnel(),
5000,
0.01);
//插入
IntStream.range(0, 100_000).forEach(filter::put);
//判斷是否存在
boolean b = filter.mightContain(1);
-
arg1
:用於將任意類型 T 的輸入數據轉化爲 Java 基本類型的數據,這裏轉換爲 byte -
arg2
:byte 字節數組的基數 -
arg3
:期望的假陽性概率
2.估計最優 m 值和 k 值
-
guava 在底層對 byte 數組的基數(m)和哈希函數的個數 k 做了自己的算法,源碼如下:
//m值的計算
static long optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
//k值的計算
static int optimalNumOfHashFunctions(long n, long m) {
// (m / n) * log(2), but avoid truncation due to division!
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
-
想要理解 guava 的計算原理,還要從的上面推導的過程繼續。
-
由假陽性率的近似計算方法可知,如果要使假陽性率儘量小,在 m 和 n 給定的情況下,
k
值應爲: -
將 k 代入上一節的式子並化簡,我們可以整理出期望假陽性率 p 與 m、n 的關係:
-
換算而得:
- 根據以上分析得出以下的結論:
-
如果指定期望假陽性率 p,那麼最優的 m 值與期望元素數 n 呈線性關係。
-
最優的 k 值實際上只與 p 有關,與 m 和 n 都無關,即:
-
綜上兩個結論,在創建布隆過濾器的時候,確定
p
值和m
值很重要。
-
總結
-
至此,布隆過濾器的知識介紹到這裏,如果覺得陳某寫得不錯的,轉發在看點一波,讀者的一份支持將會是我莫大的鼓勵。
-
另外陳某爲大家準備了500份私藏電子書和大量視頻教程,噓,私密喲,關注微信公衆號即可獲取!!