1. 程式人生 > >如何從10億資料中快速判斷是否存在某一個元素

如何從10億資料中快速判斷是否存在某一個元素

# 前言 當 `Redis` 用作快取時,其目的就是為了減少資料庫訪問頻率,降低資料庫壓力,但是假如我們某些資料並不存在於 `Redis` 當中,那麼請求還是會直接到達資料庫,而一旦在同一時間大量快取失效或者一個不存在快取的請求被惡意攻擊訪問,這些都會導致資料庫壓力驟增,這又該如何防止呢? # 快取雪崩 快取雪崩指的是 `Redis` 當中的大量快取在同一時間全部失效,而假如恰巧這一段時間同時又有大量請求被髮起,那麼就會造成請求直接訪問到資料庫,可能會把資料庫沖垮。 快取雪崩一般形容的是快取中沒有而資料庫中有的資料,而因為時間到期導致請求直達資料庫。 ## 解決方案 解決快取雪崩的方法有很多,常用的有以下幾種: - 加鎖,保證單執行緒訪問快取。這樣就不會有很多請求同時訪問到資料庫。 - `key` 值的失效時間不要設定成一樣。典型的就是初始化預熱資料的時候,將資料存入快取時可以採用隨機時間來確保不會在同一時間有大量快取失效。 - 記憶體允許的情況下,可以將快取設定為永不失效。 # 快取擊穿 快取擊穿和快取雪崩很類似,區別就是快取擊穿一般指的是單個快取失效,而同一時間又有很大的併發請求需要訪問這個 `key`,從而造成了資料庫的壓力。 ## 解決方案 解決快取擊穿的方法和解決快取雪崩的方法很類似: - 加鎖,保證單執行緒訪問快取。這樣第一個請求到達資料庫後就會重新寫入快取,後續的請求就可以直接讀取快取。 - 記憶體允許的情況下,可以將快取設定為永不失效。 # 快取穿透 快取穿透和上面兩種現象的本質區別就是這時候訪問的資料不但在 `Redis` 中不存在,而且在資料庫中也不存在,這樣如果併發過大就會造成資料來源源不斷的到達資料庫,給資料庫造成極大壓力。 ## 解決方案 對於快取穿透問題,加鎖並不能起到很好地效果,因為本身 `key` 就是不存在,所以即使控制了執行緒的訪問數,但是請求還是會源源不斷的到達資料庫。 解決快取穿透問題一般可以採用以下方案配合使用: - 介面層進行校驗,發現非法的 `key` 直接返回。比如資料庫中採用的是自增 `id`,那麼如果來了一個非整型的 `id` 或者負數 `id` 可以直接返回,或者說如果採用的是 `32` 位 `uuid`,那麼發現 `id` 長度不等於 `32` 位也可以直接返回。 - 將不存在的資料也進行快取,可以直接快取一個空或者其他約定好的無效 `value`。採用這種方案最好將 `key` 設定一個短期失效時間,否則大量不存在的 `key` 被儲存到 `Redis` 中,也會佔用大量記憶體。 # 布隆過濾器(Bloom Filter) 針對上面快取穿透的解決方案,我們思考一下:假如一個 `key` 可以繞過第 `1` 種方法的校驗,而此時有大量的不存在 `key` 被訪問(如 `1` 億個或者 `10` 億個),那麼這時候全部儲存到記憶體中,是不太現實的。 那麼有沒有一種更好的解決方案呢?這就是我們接下來要介紹的布隆過濾器,布隆過濾器就可以用盡可能小的空間儲存儘可能多的資料。 ## 什麼是布隆過濾器 布隆過濾器(Bloom Filter)是由布隆在 `1970` 年提出的。它實際上是一個很長的二進位制向量(點陣圖)和一系列隨機對映函式(雜湊函式)。 布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都比一般的演算法要好的多,缺點是有一定的誤識別率而且刪除困難。 ## 點陣圖(Bitmap) `Redis` 當中有一種資料結構就是點陣圖,布隆過濾器其中重要的實現就是點陣圖的實現,也就是位陣列,並且在這個陣列中每一個位置只有 `0` 和 `1` 兩種狀態,每個位置只佔用 `1` 個位元組,其中 `0` 表示沒有元素存在,`1` 表示有元素存在。如下圖所示就是一個簡單的布隆過濾器示例(**一個 `key` 值經過雜湊運算和位運算就可以得出應該落在哪個位置**): ![](https://img2020.cnblogs.com/blog/2232223/202102/2232223-20210226111125089-216645722.png) ## 雜湊碰撞 上面我們發現,`lonely`和`wolf`落在了同一個位置,這種不同的key值經過雜湊運算後得到相同值的現象就稱之為**雜湊碰撞**。發生雜湊碰撞之後再經過位運算,那麼最後肯定會落在同一個位置。 如果發生過多的雜湊碰撞,就會影響到判斷的準確性,所以為了減少雜湊碰撞,我們一般會綜合考慮以下 `2` 個因素: - 增大點陣圖陣列的大小(點陣圖陣列越大,佔用的記憶體越大)。 - 增加雜湊函式的次數(同一個 `key` 值經過 `1` 個函式相等了,那麼經過 `2` 個或者更多個雜湊函式的計算,都得到相等結果的概率就自然會降低了)。 上面兩個方法我們需要綜合考慮:比如增大位陣列,那麼就需要消耗更多的空間,而經過越多的雜湊計算也會消耗 `cpu` 影響到最終的計算時間,所以位陣列到底多大,雜湊函式次數又到底需要計算多少次合適需要具體情況具體分析。 ## 布隆過濾器的 2 大特點 下圖這個就是一個經過了 `2` 次雜湊函式得到的布隆過濾器,根據下圖我們很容易看到,假如我們的 `Redis` 根本不存在,但是 `Redis` 經過 `2` 次雜湊函式之後得到的兩個位置已經是 `1` 了(一個是 `wolf` 通過 `f2` 得到,一個是 `Nosql` 通過 `f1` 得到,這就是發生了雜湊碰撞,也是布隆過濾器可能存在誤判的原因)。 ![](https://img2020.cnblogs.com/blog/2232223/202102/2232223-20210226111149383-1848091716.png) 所以通過上面的現象,我們從布隆過濾器的角度可以得出布隆過濾器主要有 `2` 大特點: 1. 如果布隆過濾器判斷一個元素存在,那麼這個元素**可能存在**。 2. 如果布隆過濾器判斷一個元素不存在,那麼這個元素**一定不存在**。 而從元素的角度也可以得出 `2` 大特點: 1. 如果元素實際存在,那麼布隆過濾器**一定會判斷存在**。 2. 如果元素不存在,那麼布隆過濾器**可能會判斷存在**。 PS:**需要注意的是,如果經過 `N` 次雜湊函式,則需要得到的 `N` 個位置都是 `1` 才能判定存在,只要有一個是 `0`,就可以判定為元素不存在布隆過濾器中**。 ### fpp 因為布隆過濾器中總是會存在誤判率,因為雜湊碰撞是不可能百分百避免的。**布隆過濾器對這種誤判率稱之為假陽性概率**,即:False Positive Probability,簡稱為 `fpp`。 在實踐中使用布隆過濾器時可以自己定義一個 `fpp`,然後就可以根據布隆過濾器的理論計算出需要多少個雜湊函式和多大的位陣列空間。需要注意的是這個 `fpp` 不能定義為 `100%`,因為無法百分保證不發生雜湊碰撞。 ## 布隆過濾器的實現(Guava) 在 `Guava` 的包中提供了布隆過濾器的實現,下面就通過 `Guava` 來體會一下布隆過濾器的應用: 1. 引入 `pom` 依賴 ```java ``` 2. 新建一個測試類 `BloomFilterDemo`: ```java package com.lonely.wolf.note.redis; import com.google.common.base.Charsets; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; import java.util.UUID; public class GuavaBloomFilter { private static final int expectedInsertions = 1000000; public static void main(String[] args) { BloomFilter