前面我們提到,在防止緩存穿透的情況(緩存穿透是指,緩存和數據庫都沒有的數據,被大量請求,比如訂單號不可能為-1
,但是用戶請求了大量訂單號為-1
的數據,由於數據不存在,緩存就也不會存在該數據,所有的請求都會直接穿透到數據庫。),我們可以考慮使用布隆過濾器,來過濾掉絕對不存於集合中的元素。
布隆過濾器是什么呢?
布隆過濾器(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的,它實際上是由一個很長的二進制向量和一系列隨機hash映射函數組成(說白了,就是用二進制數組存儲數據的特征)。可以使用它來判斷一個元素是否存在於集合中,它的優點在於查詢效率高,空間小,缺點是存在一定的誤差,以及我們想要剔除元素的時候,可能會相互影響。
也就是當一個元素被加入集合的時候,通過多個hash函數,將元素映射到位數組中的k個點,置為1。
為什么需要布隆過濾器?
一般情況下,我們想要判斷是否存在某個元素,一開始考慮肯定是使用數組,但是使用數組的情況,查找的時候效率比較慢,要判斷一個元素不存在於數組中,需要每次遍歷完所有的元素。刪除完一個元素后,還得把后面的其他元素往前面移動。
其實可以考慮使用hash
表,如果有hash
表來存儲,將是以下的結構:
但是這種結構,雖然滿足了大部分的需求,可能存在兩點缺陷:
- 只有一個hash函數,其實兩個元素hash到一塊,也就是產生hash沖突的可能性,還是比較高的。雖然可以用拉鏈法(后面跟着一個鏈表)的方式解決,但是操作時間復雜度可能有所升高。
- 存儲的時候,我們需要把元素引用給存儲進去,要是上億的數據,我們要將上億的數據存儲到一個hash表里面,不太建議這樣操作。
對於上面存在的缺陷,其實我們可以考慮,用多個hash函數來減少沖突(注意:沖突時不可以避免的,只能減少),用位來存儲每一個hash值。這樣既可以減少hash沖突,還可以減少存儲空間。
假設有三個hash函數,那么不同的元素,都會使用三個hash函數,hash到三個位置上。
假設后面又來了一個張三,那么在hash的時候,同樣會hash到以下位置,所有位都是1,我們就可以說張三已經存在在里面了。
那么有沒有可能出現誤判的情況呢?這是有可能的,比如現在只有張三,李四,王五,蔡八,hash映射值如下:
后面來了陳六,但是不湊巧的是,它hash的三個函數hash出來的位,剛剛好就是被別的元素hash之后,改成1了,判斷它已經存在了,但是實際上,陳六之前是不存在的。
上面的情況,就是誤判,布隆過濾器都會不可避免的出現誤判。但是它有一個好處是,布隆過濾器,判斷存在的元素,可能不存在,但是判斷不存在的元素,一定不存在。,因為判斷不存在說明至少有一位hash出來是對不上的。
也是由於會出現多個元素可能hash到一起,但有一個數據被踢出了集合,我們想把它映射的位,置為0,相當於刪除該數據。這個時候,就會影響到其他的元素,可能會把別的元素映射的位,置為了0。這也就是為什么布隆過濾器不能刪除的原因。
具體步驟
添加元素:
-
- 使用多個hash函數對元素item進行hash運算,得到多個hash值。
-
- 每一個hash值對bit位數組取模,得到位數組中的位置索引index。
-
- 如果index的位置不為1,那么就將該位置為1。
判斷元素是否存在:
-
- 使用多個hash函數對元素item進行hash運算,得到多個hash值。
-
- 每一個hash值對bit位數組取模,得到位數組中的位置索引index。
-
- 如果index所處的位置都為1,說明元素可能已經存在了。
誤判率推導
慶幸的是,布隆過濾器的誤判率是可以預測的,由上面的分析,也可以得知,其實是與位數組的大小,以及hash函數的個數等,這些都是息息相關的。
假設位數組的大小是m,我們一共有k個hash函數,那么每一個hash函數,進行hash的時候,只能hash到m位中的一個位置,所以沒有被hash到的概率是:
$$1-\frac{1}{m}$$
k個hash函數都hash之后,該位還是沒有被hash到1的概率是:
$$(1-\frac{1}{m})^k$$
如果我們插入了n個元素,也就是hash了n*k次,該位還是沒有被hash成1的概率是:
$$(1-\frac{1}{m})^{kn}$$
那該位為1的概率就是:
$$1-(1-\frac{1}{m})^{kn}$$
如果需要檢測某一個元素是不是在集合中,也就是該元素對應的k個hash元素hash出來的值,都需要設置為1。也就是該元素不存在,但是該元素對應的所有位都被hash成為1的概率是:
$${(1-(1-\frac{1}{m}){kn})}{k}\approx {(1-e{-kn/m})}k $$
可以大致看出,隨着位數組大小m和hash函數個數的增加,其實概率會下降,隨着插入的元素n的增加,概率會有所上升。
最后也可以通過自己期待的誤判率P和期待添加的個數n,來大致計算出布隆過濾器的位數組的長度:
$$m=-(\frac{nInP}{(In2)^2})$$
上面就是誤判率的大致計算方式,同時也提示我們,可以根據自己業務的數據量以及誤判率,來調整我們的數組的大小。
布隆過濾器的作用
除了我們前面說的過濾爬蟲惡意請求,還可以對一些URL進行去重,過濾海量數據里面的重復數據,過濾數據庫里面不存在的id等等。
但是,即使有布隆過濾器,我們也不可能完全避免,或者徹底解決緩存穿透這個問題。只是相當於做了優化,將准確率提高。
很多的key-value數據庫也會使用布隆過濾器來加快查詢效率,因為全部挨個判斷一遍,這個效率太低了。
【刷題筆記】
Github倉庫地址:https://github.com/Damaer/codeSolution
筆記地址:https://damaer.github.io/codeSolution/
【作者簡介】:
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java源碼解析,JDBC,Mybatis,Spring,redis,分布式,劍指Offer,LeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花里胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查找資料。遺漏或者錯誤之處,還望指正。
平日時間寶貴,只能使用晚上以及周末時間學習寫作,關注我,我們一起成長吧~