1. 程式人生 > 其它 >海量資料處理利器之布隆過濾器

海量資料處理利器之布隆過濾器

      看見了海量資料去重,找到停留時間最長的IP等問題,有博友提到了Bloom Filter,我就查了查,不過首先想到的是大叔,下面就先看看大叔的風采。

一、布隆過濾器概念引入

      (Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的。它實際上是由一個很長的二進位制向量和一系列隨機對映函式組成,布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的演算法,缺點是有一定的誤識別率(假正例False positives,即Bloom Filter報告某一元素存在於某集合中,但是實際上該元素並不在集合中)和刪除困難,但是沒有識別錯誤的情形(即假反例False negatives,如果某個元素確實在該集合中,那麼Bloom Filter 是不會報告該元素不存在於集合中的,所以不會漏報)。

下面從簡單的排序談到BitMap演算法,再談到資料去重問題,談到大資料量處理利器:布隆過濾器。

  • 對無重複的資料進行排序

      給定資料(2,4,1,12,9,7,6)如何對它排序?

     方法1:基本的排序方法包括冒泡,快排等。

     方法2:使用BitMap演算法

     方法1就不介紹了,方法2中所謂的BitMap是一個位數組,跟平時使用的陣列的唯一差別在於操作的是位。首先是開闢2個位元組大小的位陣列,長度為12(該長度由上述資料中最大的數字12決定的),然後,讀取資料,2存放在位陣列中下標為1的地方,值從0改為1,4存放在下標為3的地方,值從0改為1....最後,讀取該位陣列,得到排好序的資料是:(1,2,4,6,7,9,12)。

      比較方法1和方法2的差別:方法2中,排序需要的時間複雜度和空間複雜度很依賴與資料中最大的數字比如12,因此空間上講需要開2個位元組大小的記憶體,時間上需要遍歷完整個陣列。當資料類似(1,1000,10萬)只有3個數據的時候,顯然用方法2,時間複雜度和空間複雜度相當大,但是當資料比較密集時該方法就會顯示出來優勢。

  • 對有重複的資料進行判重

   資料(2,4,1,12,2,9,7,6,1,4)如何找出重複出現的數字?

   首先是開闢2個位元組大小的位陣列,長度為12(該長度由上述資料中最大的數字12決定的,當讀取完12後,當讀取2的時候,發現數組中的值是1,則判斷出2是重複出現的。

二、布隆過濾器原理

      布隆過濾器需要的是一個位數組(這個和點陣圖有點類似)和k個對映函式(和Hash表類似),在初始狀態時,對於長度為m的位陣列array,它的所有位都被置為0。對於有n個元素的集合S={s1,s2......sn},通過k個對映函式{f1,f2,......fk},將集合S中的每個元素sj(1<=j<=n)對映為k個值{g1,g2......gk},然後再將位陣列array中相對應的array[g1],array[g2]......array[gk]置為1;如果要查詢某個元素item是否在S中,則通過對映函式{f1,f2.....fk}得到k個值{g1,g2.....gk},然後再判斷array[g1],array[g2]......array[gk]是否都為1,若全為1,則item在S中,否則item不在S中。這個就是布隆過濾器的實現原理。

      當然有讀者可能會問:即使array[g1],array[g2]......array[gk]都為1,能代表item一定在集合S中嗎?不一定,因為有這個可能:就是集合中的若干個元素通過對映之後得到的數值恰巧包括g1,g2,.....gk,那麼這種情況下可能會造成誤判,但是這個概率很小,一般在萬分之一以下。

      很顯然,布隆過濾器的誤判率和這k個對映函式的設計有關,到目前為止,有很多人設計出了很多高效實用的hash函式。尤其要注意的是,布隆過濾器是不允許刪除元素的(實際就是因為多個str可能都應設在同一點,而判斷str存在的話是所有對映點都存在,所以不能刪除),因為若刪除一個元素,可能會發生漏判的情況。不過有一種布隆過濾器的變體Counter Bloom Filter,可以支援刪除元素,感興趣的讀者可以查閱相關文獻資料。

三、布隆過濾器False positives 概率推導

      假設 Hash 函式以等概率條件選擇並設定 Bit Array 中的某一位,m 是該位陣列的大小,k 是 Hash 函式的個數,那麼位陣列中某一特定的位在進行元素插入時的 Hash 操作中沒有被置位為1的概率是:

;那麼在所有 k 次 Hash 操作後該位都沒有被置 "1" 的概率是:

;如果我們插入了 n 個元素,那麼某一位仍然為 "0" 的概率是:

因而該位為 "1"的概率是:

;現在檢測某一元素是否在該集合中。標明某個元素是否在集合中所需的 k 個位置都按照如上的方法設定為 "1",但是該方法可能會使演算法錯誤的認為某一原本不在集合中的元素卻被檢測為在該集合中(False Positives),該概率由以下公式確定:

      其實上述結果是在假定由每個 Hash 計算出需要設定的位(bit) 的位置是相互獨立為前提計算出來的,不難看出,隨著 m (位陣列大小)的增加,假正例(False Positives)的概率會下降,同時隨著插入元素個數 n 的增加,False Positives的概率又會上升,對於給定的m,n,如何選擇Hash函式個數 k 由以下公式確定:

;此時False Positives的概率為:

;而對於給定的False Positives概率 p,如何選擇最優的位陣列大小 m 呢,

;該式表明,位陣列的大小最好與插入元素的個數成線性關係,對於給定的 m,n,k,假正例概率最大為:

四、布隆過濾器應用

      布隆過濾器在很多場合能發揮很好的效果,比如:網頁URL的去重,垃圾郵件的判別,集合重複元素的判別,查詢加速(比如基於key-value的儲存系統)等,下面舉幾個例子:

  • 有兩個URL集合A,B,每個集合中大約有1億個URL,每個URL佔64位元組,有1G的記憶體,如何找出兩個集合中重複的URL。

很顯然,直接利用Hash表會超出記憶體限制的範圍。這裡給出兩種思路:

      第一種:如果不允許一定的錯誤率的話,只有用分治的思想去解決,將A,B兩個集合中的URL分別存到若干個檔案中{f1,f2...fk}和{g1,g2....gk}中,然後取f1和g1的內容讀入記憶體,將f1的內容儲存到hash_map當中,然後再取g1中的url,若有相同的url,則寫入到檔案中,然後直到g1的內容讀取完畢,再取g2...gk。然後再取f2的內容讀入記憶體。。。依次類推,知道找出所有的重複url。

      第二種:如果允許一定錯誤率的話,則可以用布隆過濾器的思想。

  • 在進行網頁爬蟲時,其中有一個很重要的過程是重複URL的判別,如果將所有的url存入到資料庫中,當資料庫中URL的數

量很多時,在判重時會造成效率低下,此時常見的一種做法就是利用布隆過濾器,還有一種方法是利用berkeley db來儲存url,Berkeley db是一種基於key-value儲存的非關係資料庫引擎,能夠大大提高url判重的效率。

      布隆過濾器主要運用在過濾惡意網址用的,將所有的惡意網址建立在一個布隆過濾器上,然後對使用者的訪問的網址進行檢測,如果在惡意網址中那麼就通知使用者。這樣的話,我們還可以對一些常出現判斷錯誤的網址設定一個白名單,然後對出現判斷存在的網址再和白名單中的網址進行匹配,如果在白名單中,那麼就放行。當然這個白名單不能太大,也不會太大,布隆過濾器錯誤的概率是很小的。

五、布隆過濾器簡單Java實現

package a;

import java.util.BitSet;
/*
 * 存在的問題
 * DEFAULT_LEN長度設定為多少合適呢?
 * 我發現result和DEFAULT_LEN有關,不應該啊,沒發現原因
 */
public class BloomFilterTest {
	//30位,表示2^2^30種字元
	static int DEFAULT_LEN = 1<<30;
	//要用質數
	static int[] seeds = {3,5,7,11,17,31};
	static BitSet bitset = new BitSet(DEFAULT_LEN); 
	static MyHash[] myselfHash = new MyHash[seeds.length];
	
	
	
	public static void main(String[] args) {
		String str = "[email protected]";
		
		//生成一次就夠了
		for(int i=0; i<seeds.length; i++) {
			myselfHash[i] = new MyHash(DEFAULT_LEN, seeds[i]);
		}
		bitset.clear();
		for(int i=0; i<myselfHash.length; i++) {
			bitset.set(myselfHash[i].myHash(str),true);
		}
		boolean flag = containsStr(str);
		//System.out.println("========================");
		System.out.println(flag);
			
	}

	private static boolean containsStr(String str) {
		// TODO Auto-generated method stub
		if(null==str)
			return false;
		for(int i=0; i<seeds.length; i++) {
			if(bitset.get(myselfHash[i].myHash(str))==false)
				return false;
		}
		return true;
	}
}
class MyHash {
	int len;
	int seed;
	
	public MyHash(int len, int seed) {
		super();
		this.len = len;
		this.seed = seed;
	}
	
	public int myHash(String str) {
		int len = str.length();
		int result = 0;
		//這的len就是str的len,不是成員變數的len
		for(int i=0; i<len; i++) {
			//System.out.println(seed+"oooooooooooo");
			result = result*seed + str.charAt(i);
			//System.out.println(result);
			//長度就是1<<24,如果大於這個數 感覺結果不準確
			//<0就是大於了0x7ffffff
			if(result>(1<<30) || result<0) {
				//System.out.println("-----"+(1<<30));
				System.out.println(result+"myHash資料越界!!!");
				break;
			}
		}
		return (len-1)&result;
	}
}