在前一篇文章中咱們介紹了散列表和BitMap的相關概念與部分應用。本文將會具體講解BitMap的擴展:布隆過濾器(Bloom filter)。html
Hash表實際上爲每個可能出現的數字提供了一個一一映射的關係,每一個元素都至關於有了本身的獨享的一份空間,這個映射由散列函數來提供。Hash表甚至還能記錄每一個元素出現的次數,利用這一點能夠實現更復雜的功能。咱們的需求是集合中每一個元素有一個獨享的空間而且能找到一個到這個空間的映射方法。獨享的空間對於咱們的問題來講,一個Boolean就夠了,或者說,1個bit就夠了,咱們只想知道某個元素出現過沒有。若是爲每一個全部可能的值分配1個bit,這就是BitMap所要完成的工做。然而當數據量大到必定程度,所須要的存儲空間將會超出可承受的範圍,如寫64bit類型的數據,須要大概2EB存儲。java
布隆過濾器(Bloom Filter)是1970年由布隆提出的。布隆過濾器能夠用於檢索一個元素是否在一個集合中。布隆過濾器是一種空間效率極高的機率型算法和數據結構,它其實是一個很長的二進制向量和一系列隨機映射函數。BitMap對於每個可能的整型值,經過直接尋址的方式進行映射,至關於使用了一個哈希函數,而布隆過濾器就是引入了k(k>1)個相互獨立的哈希函數,保證在給定的空間、誤判率下,完成元素判重的過程。redis
具體來看Bloom Filter是如何用位數組表示集合的。初始狀態時,Bloom Filter是一個包含m位的位數組,每一位都置爲0。算法
Bloom Filter使用k個相互獨立的哈希函數(Hash Function),它們分別將集合中的每一個元素映射到{1,…,m}
的範圍中。對任意一個元素x,第i個哈希函數映射的位置hash_i(x)就會被置爲1(1≤i≤k)。數據庫
當一個元素被加入集合中時,經過k各散列函數將這個元素映射成一個位數組中的k個點,並將這k個點所有置爲1。下圖是k=3時的布隆過濾器。網頁爬蟲
x、y、z經由哈希函數映射將各自在Bitmap中的3個位置置爲1,當w出現時,僅當3個標誌位都爲1時,才表示w在集合中。圖中所示的狀況,布隆過濾器將斷定w不在集合中。後端
Bloom Filter有必定的誤判率。在判斷一個元素是否屬於某個集合時,有可能會把不屬於這個集合的元素誤判爲屬於這個集合。所以,它不適合那些"零誤判"的應用場合。在能容忍低誤判的應用場景下,布隆過濾器經過極少的誤判換區了存儲空間的極大節省。數組
那麼布隆過濾器的偏差有多少?咱們假設全部哈希函數散列足夠均勻,散列後落到Bitmap每一個位置的機率均等。Bitmap的大小爲m、原始數集大小爲n、哈希函數個數爲k:緩存
某一位置爲1的機率爲:安全
布隆過濾器的最大的用處就是,可以迅速判斷一個元素是否在一個集合中。所以他有以下三個使用場景:
網頁爬蟲對URL的去重,避免爬取相同的URL地址
反垃圾郵件,從數十億個垃圾郵件列表中判斷某郵箱是否垃圾郵箱(同理,垃圾短信)
緩存擊穿,將已存在的緩存放到布隆過濾器中,當黑客訪問不存在的緩存時迅速返回避免緩存及DB掛掉。
緩存系統中,按照KEY去查詢VALUE,當KEY對應的VALUE必定不存在的時候並對KEY併發請求量很大的時候,就會對後端形成很大的壓力。若是緩存集中在一段時間內失效,發生大量的緩存穿透,全部的查詢都落在數據庫上,形成了緩存雪崩。
因爲緩存不命中,每次都要查詢持久層。從而失去緩存的意義。
這裏只要增長一個bloom算法的服務,服務端插入一個key時,在這個服務中設置一次。須要查詢服務端時,先判斷key在後端是否存在,這樣就能避免服務端的壓力。
下面咱們介紹使用Google實現的BloomFilter
。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
複製代碼
private static int size = 1000000;
private static BloomFilter<Integer> bloomFilter =
BloomFilter.create(Funnels.integerFunnel(), size);
@Test
public void consumeTest() {
for (int i = 0; i < size; i++) {
bloomFilter.put(i);
}
long startTime = System.nanoTime(); // 獲取開始時間
// 判斷這一百萬個數中是否包含29999這個數
if (bloomFilter.mightContain(29999)) {
System.out.println("命中了");
}
long endTime = System.nanoTime(); // 獲取結束時間
System.out.println("程序運行時間: " + (endTime - startTime) + "納秒");
}
複製代碼
使用BloomFilter
查找一個元素29999,很是快速。
private static int size = 1000000;
private static BloomFilter<Integer> bloomFilter =
BloomFilter.create(Funnels.integerFunnel(), size);
@Test
public void errorTest() {
for (int i = 0; i < size; i++) {
bloomFilter.put(i);
}
List<Integer> list = new ArrayList<>(1000);
// 取10000個不在過濾器裏的值,看看有多少個會被認爲在過濾器裏
for (int i = size + 10000; i < size + 20000; i++) {
if (bloomFilter.mightContain(i)) {
list.add(i);
}
}
System.out.println("誤判的數量:" + list.size());
}
複製代碼
上述代碼所示,咱們取10000個不在過濾器裏的值,卻還有330個被認爲在過濾器裏,這說明了誤判率爲0.03。即,在不作任何設置的狀況下,默認的誤判率爲0.03。 BloomFilter
默認的構造函數以下:
@CheckReturnValue
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
return create(funnel, expectedInsertions, 0.03D);
}
複製代碼
固然咱們能夠經過以下的構造函數,手動設置誤判率。
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size,0.01);
複製代碼
static int sizeOfNumberSet = Integer.MAX_VALUE >> 4;
static Random generator = new Random();
@Test
public void actualTest() {
int error = 0;
HashSet<Integer> hashSet = new HashSet<Integer>();
BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), sizeOfNumberSet);
System.out.println(sizeOfNumberSet);
for (int i = 0; i < sizeOfNumberSet; i++) {
int number = generator.nextInt();
if (filter.mightContain(number) != hashSet.contains(number)) {
error++;
}
filter.put(number);
hashSet.add(number);
}
System.out.println(
"Error count: "
+ error
+ ", error rate = "
+ String.format("%f", (float) error / (float) sizeOfNumberSet));
}
複製代碼
BloomFilter
實際的應用相似如上所示,換成redis客戶端調用便可,用於redis緩存擊穿等場景。
本文主要講了布隆過濾器相關概念、算法描述、錯誤率統計和布隆過濾器的實現與應用。布隆過濾器是BitMap的一種工業實現,解決了使用BitMap時當數據量大到必定程度,所須要的存儲空間將會超出可承受的範圍的問題。
布隆過濾器就是引入了k(k>1)個相互獨立的哈希函數,保證在給定的空間、誤判率下,完成元素判重的過程。布隆過濾器有一個誤判率的概念,誤判率越低,則數組越長,所佔空間越大。誤判率越高則數組越小,所佔的空間越小。最後,咱們經過Google實現的BloomFilter
,介紹如何使用布隆過濾器並自定義調整誤判率。
相比於其它的數據結構,布隆過濾器在空間和時間方面都有巨大的優點。布隆過濾器存儲空間和插入/查詢時間都是常數(O(k))。哈希表也能用於判斷元素是否在集合中,可是布隆過濾器只須要哈希表的1/8或1/4的空間複雜度就能完成一樣的問題。
布隆過濾器的缺點除了誤算率以外(隨着存入的元素數量增長,誤算率隨之增長。可是若是元素數量太少,則使用散列表足矣),不能從布隆過濾器中刪除元素。咱們很容易想到把位數組變成整數數組,每插入一個元素相應的計數器加1, 這樣刪除元素時將計數器減掉就能夠了。然而要保證安全地刪除元素並不是如此簡單。首先咱們必須保證刪除的元素的確在布隆過濾器裏面。這一點單憑這個過濾器是沒法保證的。
最後,歡迎購買筆者的新書《Spring Cloud微服務架構進階》。