1. 程式人生 > >【實戰問題】-- 快取穿透之布隆過濾器(1)

【實戰問題】-- 快取穿透之布隆過濾器(1)

前面我們提到,在防止快取穿透的情況(快取穿透是指,**快取和資料庫都沒有的資料**,被大量請求,比如訂單號不可能為`-1`,但是使用者請求了大量訂單號為`-1`的資料,由於資料不存在,快取就也不會存在該資料,所有的請求都會直接穿透到資料庫。),我們可以考慮使用布隆過濾器,來過濾掉絕對不存於集合中的元素。 ## 布隆過濾器是什麼呢? 布隆過濾器(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的,它實際上是由一個很長的二進位制向量和一系列隨機hash對映函式組成(說白了,就是用二進位制陣列儲存資料的特徵)。可以使用它來判斷一個元素是否存在於集合中,它的優點在於查詢效率高,空間小,缺點是存在一定的誤差,以及我們想要剔除元素的時候,可能會相互影響。 也就是當一個元素被加入集合的時候,通過多個hash函式,將元素對映到位陣列中的k個點,置為1。 ## 為什麼需要布隆過濾器? 一般情況下,我們想要判斷是否存在某個元素,一開始考慮肯定是使用陣列,但是使用陣列的情況,查詢的時候效率比較慢,要判斷一個元素不存在於陣列中,需要每次遍歷完所有的元素。刪除完一個元素後,還得把後面的其他元素往前面移動。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308225741.png) 其實可以考慮使用`hash`表,如果有`hash`表來儲存,將是以下的結構: ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308230509.png) 但是這種結構,雖然滿足了大部分的需求,可能存在兩點缺陷: - 只有一個hash函式,其實兩個元素hash到一塊,也就是產生hash衝突的可能性,還是比較高的。雖然可以用拉鍊法(後面跟著一個連結串列)的方式解決,但是操作時間複雜度可能有所升高。 - 儲存的時候,我們需要把元素引用給儲存進去,要是上億的資料,我們要將上億的資料儲存到一個hash表裡面,不太建議這樣操作。 對於上面存在的缺陷,其實我們可以考慮,用多個hash函式來減少衝突(注意:衝突時不可以避免的,只能減少),用位來儲存每一個hash值。這樣既可以減少hash衝突,還可以減少儲存空間。 假設有三個hash函式,那麼不同的元素,都會使用三個hash函式,hash到三個位置上。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308233116.png) 假設後面又來了一個張三,那麼在hash的時候,同樣會hash到以下位置,所有位都是1,我們就可以說張三已經存在在裡面了。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308233607.png) 那麼有沒有可能出現誤判的情況呢?這是有可能的,比如現在只有張三,李四,王五,蔡八,hash對映值如下: ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308233752.png) 後面來了陳六,但是不湊巧的是,它hash的三個函式hash出來的位,剛剛好就是被別的元素hash之後,改成1了,判斷它已經存在了,但是實際上,陳六之前是不存在的。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308233900.png) 上面的情況,就是誤判,布隆過濾器都會不可避免的出現誤判。但是它有一個好處是,**布隆過濾器,判斷存在的元素,可能不存在,但是判斷不存在的元素,一定不存在。**,因為判斷不存在說明至少有一位hash出來是對不上的。 也是由於會出現多個元素可能hash到一起,但有一個數據被踢出了集合,我們想把它對映的位,置為0,相當於刪除該資料。這個時候,就會影響到其他的元素,可能會把別的元素對映的位,置為了0。這也就是為什麼布隆過濾器不能刪除的原因。 ## 具體步驟 新增元素: - 1. 使用多個hash函式對元素item進行hash運算,得到多個hash值。 - 2. 每一個hash值對bit位陣列取模,得到位陣列中的位置索引index。 - 3. 如果index的位置不為1,那麼就將該位置為1。 判斷元素是否存在: - 1. 使用多個hash函式對元素item進行hash運算,得到多個hash值。 - 2. 每一個hash值對bit位陣列取模,得到位陣列中的位置索引index。 - 3. 如果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等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。 [2020年我寫了什麼?](http://aphysia.cn/archives/2020) [開源刷題筆記](https://damaer.github.io/CodeSolution/#/) 平日時間寶貴,只能使用晚上以及週末時間學習寫作,關注我,我們一起