假若有一個15億用戶的系統,天天有幾億用戶訪問系統,要如何快速判斷是否爲系統中的用戶呢?java
還有對於網站爬蟲的項目,咱們都知道世界上的網站數量及其之多,每當咱們爬一個新的網站url時,如何快速判斷是否爬蟲過了呢?還有垃圾郵箱的過濾,廣告電話的過濾等等。若是仍是用上面2種方法,顯然不是最好的解決方案。mysql
再者,查詢是一個系統最高頻的操做,當查詢一個數據,首先會先到緩存查詢(例如Redis),若是緩存沒命中,因而到持久層數據庫(mongo,mysql等)查詢,發現也沒有此數據,因而本此查詢失敗。若是用戶不少的時候,而且緩存都沒命中,進而所有請求了持久層數據庫,這就給數據庫帶來很大壓力,嚴重可能拖垮數據庫。俗稱緩存穿透
。面試
可能你們也聽到另外一個詞叫緩存擊穿
,它是指一個熱點key,不停着扛着高併發,忽然這個key失效了,在失效的瞬間,大量的請求緩存就沒命中,所有請求到數據庫。redis
對於以上這些以及相似的場景,如何高效的解決呢?針對此,布隆過濾器應運而生了。算法
布隆過濾器(Bloom Filter)是1970年由布隆提出的。它其實是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器能夠用於檢索一個元素是否在一個集合中。它的優勢是空間效率和查詢時間都比通常的算法要好的多,缺點是有必定的誤識別率和刪除困難。sql
二進制向量,簡單理解就是一個二進制數組。這個數組裏面存放的值要麼是0,要麼是1。數據庫
映射函數,它能夠將一個元素映射成一個位陣列(Bit array)中的一個點。因此經過這個點,就能判斷集合中是否有此元素。api
基本思想數組
必定
不存在;若是都是1,則被檢元素極可能
存在。Bloom Filter跟單個哈希函數映射不一樣,Bloom Filter使用了k個哈希函數,每一個元素跟k個bit對應。從而下降了衝突的機率。緩存
優勢
缺點
Counting Bloom Filter
解決。在Redis中,有一種數據結構叫位圖,即bitmap
。如下是一些經常使用的操做命令。
在Redis命令中,SETBIT key offset value
,此命令表示將key對應的值的二進制數組,從左向右起,offset下標的二進制數字設置爲value。
鍵k1對應的值爲keke,對應ASCII碼爲107 101 107 101,對應的二進制爲 0110 1011,0110 0101,0110 1011,0110 0101。將下標5的位置設置爲1,因此變成 0110 1111,0110 0101,0110 1011,0110 0101。即 oeke。
GETBIT key offset
命令,它用來獲取指定下標的值。
還有一個比較經常使用的命令,BITCOUNT key [start end]
,用來獲取位圖中指定範圍值爲1的個數。注意,start和end指定的是字節的個數,而不是位數組下標。
Redisson
是用於在Java程序中操做Redis的庫,利用Redisson咱們能夠在程序中輕鬆地使用Redis。Redisson這個客戶端工具實現了布隆過濾器,其底層就是經過bitmap這種數據結構來實現的。
Redis 4.0提供了插件功能以後,Redis就提供了布隆過濾器功能。布隆過濾器做爲一個插件加載到了Redis Server之中,給Redis提供了強大的布隆去重功能。此文就不細講了,你們感興趣地可到官方查看詳細文檔介紹。它又以下經常使用命令:
下面演示是在本地單節點Redis實現的,若是數據量很大,而且偏差率又很低的狀況下,那單節點內存可能會不足。固然,在集羣Redis中,也是能夠經過Redisson實現分佈式布隆過濾器的。
引入依賴
<!-- https://mvnrepository.com/artifact/org.redisson/redisson --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version> </dependency>
代碼測試
package com.nobody; import org.redisson.Redisson; import org.redisson.api.RBloomFilter; import org.redisson.api.RedissonClient; import org.redisson.config.Config; /** * @Description * @Author Mr.nobody * @Date 2021/3/6 * @Version 1.0 */ public class RedissonDemo { public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // config.useSingleServer().setPassword("123456"); RedissonClient redissonClient = Redisson.create(config); // 獲取一個redis key爲users的布隆過濾器 RBloomFilter<Integer> bloomFilter = redissonClient.getBloomFilter("users"); // 假設元素個數爲10萬 int size = 100000; // 進行初始化,預計元素爲10萬,偏差率爲1% bloomFilter.tryInit(size, 0.01); // 將1至100000這十萬個數映射到布隆過濾器中 for (int i = 1; i <= size; i++) { bloomFilter.add(i); } // 檢查已在過濾器中的值,是否有匹配不上的 for (int i = 1; i <= size; i++) { if (!bloomFilter.contains(i)) { System.out.println("存在不匹配的值:" + i); } } // 檢查不在過濾器中的1000個值,是否有匹配上的 int matchCount = 0; for (int i = size + 1; i <= size + 1000; i++) { if (bloomFilter.contains(i)) { matchCount++; } } System.out.println("誤判個數:" + matchCount); } }
結果存在的10萬個元素都匹配上了;不存在布隆過濾器中的1千個元素,有23個誤判。
誤判個數:23
布隆過濾器有許多實現與優化,Guava中就提供了一種實現。Google Guava提供的布隆過濾器的位數組是存儲在JVM內存中,故是單機版的,而且最大位長爲int類型的最大值。
Bit數組大小選擇
根據預估數據量n以及誤判率fpp,bit數組大小的m的計算方式:
Guava中源碼實現以下:
@VisibleForTesting 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))); }
哈希函數選擇
哈希函數的個數的選擇也是挺講究的,哈希函數的選擇影響着性能的好壞,並且一個好的哈希函數能近似等機率的將元素映射到各個Bit。如何選擇構造k個函數呢,一種簡單的方法是選擇一個哈希函數,而後送入k個不一樣的參數。
哈希函數的個數k,能夠根據預估數據量n和bit數組長度m計算而來:
Guava中源碼實現以下:
@VisibleForTesting 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))); }
引入依賴
<!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.2-jre</version> </dependency>
代碼測試
package com.nobody; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; /** * @Description * @Author Mr.nobody * @Date 2021/3/6 * @Version 1.0 */ public class GuavaDemo { public static void main(String[] args) { // 假設元素個數爲10萬 int size = 100000; // 預計元素爲10萬,偏差率爲1% BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, 0.01); // 將1至100000這十萬個數映射到布隆過濾器中 for (int i = 1; i <= size; i++) { bloomFilter.put(i); } // 檢查已在過濾器中的值,是否有匹配不上的 for (int i = 1; i <= size; i++) { if (!bloomFilter.mightContain(i)) { System.out.println("存在不匹配的值:" + i); } } // 檢查不在過濾器中的1000個值,是否有匹配上的 int matchCount = 0; for (int i = size + 1; i <= size + 1000; i++) { if (bloomFilter.mightContain(i)) { matchCount++; } } System.out.println("誤判個數:" + matchCount); } }
結果存在的10萬個元素都匹配上了;不存在布隆過濾器中的1千個元素,有10個誤判。
誤判個數:10
當fpp的值改成爲0.001,即下降偏差率時,誤判個數爲0個。
誤判個數:0
分析結果可知,誤判率確實跟咱們傳入的容錯率差很少,並且在布隆過濾器中的元素都匹配到了。
源碼分析
經過debug建立布隆過濾器的方法,當預計元素爲10萬個,fpp的值爲0.01時,須要位數958505個,hash函數個數爲7個。
當預計元素爲10萬個,fpp的值爲0.001時,須要位數1437758個,hash函數個數爲10個。
得出結論
假若有一臺服務器,內存只有4GB,磁盤上有2個大文件,文件A存儲100億個URL,文件B存儲100億個URL。請問如何模糊
找出兩個文件的URL交集?如何精緻
找出兩個文件的URL交集。
模糊交集:
藉助布隆過濾器思想,先將一個文件的URL經過hash函數映射到bit數組中,這樣大大減小了內存存儲,再讀取另外一個文件URL,去bit數組中進行匹配。
精緻交集:
對大文件進行hash拆分紅小文件,例如拆分紅1000個小文件(若是服務器內存更小,則能夠拆分更多個更小的文件),好比文件A拆分爲A1,A2,A3...An,文件B拆分爲B1,B2,B3...Bn。並且經過相同的hash函數,相同的URL必定被映射到相同下標的小文件中,例如A文件的www.baidu.com被映射到A1中,那B文件的www.baidu.com也必定被映射到B1文件中。最後再經過求相同下標的小文件(例如A1和B1)(A2和B2)的交集便可。
歡迎關注微信公衆號:「Java之言」技術文章持續更新,請持續關注......
- 第一時間學習最新技術文章
- 領取最新技術學習資料視頻
- 最新互聯網資訊和麪試經驗