淺談布隆過濾器Bloom Filter

先從一道面試題開始:python

給A,B兩個文件,各存放50億條URL,每條URL佔用64字節,內存限制是4G,讓你找出A,B文件共同的URL。git

這個問題的本質在於判斷一個元素是否在一個集合中。哈希表以O(1)的時間複雜度來查詢元素,但付出了空間的代價。在這個大數據問題中,就算哈希表有100%的空間利用率,也至少須要50億*64Byte的空間,4G確定是遠遠不夠的。github

固然咱們可能想到使用位圖,每一個URL取整數哈希值,置於位圖相應的位置上。4G大概有320億個bit,看上去是可行的。但位圖適合對海量的、取值分佈很均勻的集合去重。位圖的空間複雜度是隨集合內最大元素增大而線性增大的。要設計衝突率很低的哈希函數,勢必要增長哈希值的取值範圍,假如哈希值最大取到了264,位圖大概須要23億G的空間。4G的位圖最大值是320億左右,爲50億條URL設計衝突率很低、最大值爲320億的哈希函數比較困難。面試

題目的一個解決思路是將文件切割成能夠放入4G空間的小文件,重點在於A與B兩個文件切割後的小文件要一一對應。算法

分別切割A與B文件,根據hash(URL) % k值將URL劃分到k個不一樣的文件中,如A1,A2,...,Ak和B1,B2,...,Bk,同時能夠保存hash值避免重複運算。這樣Bn小文件與A文件共同的URL確定會分到對應的An小文件中。讀取An到一個哈希表中,再遍歷Bn,判斷是否有重複的URL。數據庫

另外一個解決思路就是使用Bloom Filter布隆過濾器了。緩存

Bloom Filter簡介

布隆過濾器(Bloom-Filter)是1970年由Bloom提出的。它能夠用於檢索一個元素是否在一個集合中。服務器

布隆過濾器實際上是位圖的一種擴展,不一樣的是要使用多個哈希函數。它包括一個很長的二進制向量(位圖)和一系列隨機映射函數。網絡

首先創建一個m位的位圖,而後對於每個加入的元素,使用k個哈希函數求k個哈希值映射到位圖的k個位置,而後將這k個位置的bit全設置爲1。下圖是k=3的布隆過濾器:函數

檢索時,咱們只要檢索這些k個位是否是都是1就能夠了:若是這些位有任何一個0,則被檢元素必定不在;若是都是1,則被檢元素極可能在。

能夠看出布隆過濾器在時間和空間上的效率比較高,但也有缺點:

  • 存在誤判。布隆過濾器能夠100%肯定一個元素不在集合之中,但不能100%肯定一個元素在集合之中。當k個位都爲1時,也有多是其它的元素將這些bit置爲1的。
  • 刪除困難。一個放入容器的元素映射到位圖的k個位置上是1,刪除的時候不能簡單的直接所有置爲0,可能會影響其餘元素的判斷。

Bloom Filter實現

要實現一個布隆過濾器,咱們須要預估要存儲的數據量爲n,指望的誤判率爲P,而後計算位圖的大小m,哈希函數的個數k,並選擇哈希函數。

求位圖大小m公式:

哈希函數數目k公式:

Python中已經有實現布隆過濾器的包:pybloom

安裝

pip install pybloom

簡單的看一下實現:

class BloomFilter(object):
    FILE_FMT = b'<dQQQQ'

    def __init__(self, capacity, error_rate=0.001):
        """Implements a space-efficient probabilistic data structure
        capacity
            this BloomFilter must be able to store at least *capacity* elements
            while maintaining no more than *error_rate* chance of false
            positives
        error_rate
            the error_rate of the filter returning false positives. This
            determines the filters capacity. Inserting more than capacity
            elements greatly increases the chance of false positives.
        >>> b = BloomFilter(capacity=100000, error_rate=0.001)
        >>> b.add("test")
        False
        >>> "test" in b
        True
        """
        if not (0 < error_rate < 1):
            raise ValueError("Error_Rate must be between 0 and 1.")
        if not capacity > 0:
            raise ValueError("Capacity must be > 0")
        # given M = num_bits, k = num_slices, P = error_rate, n = capacity
        #       k = log2(1/P)
        # solving for m = bits_per_slice
        # n ~= M * ((ln(2) ** 2) / abs(ln(P)))
        # n ~= (k * m) * ((ln(2) ** 2) / abs(ln(P)))
        # m ~= n * abs(ln(P)) / (k * (ln(2) ** 2))
        num_slices = int(math.ceil(math.log(1.0 / error_rate, 2)))
        bits_per_slice = int(math.ceil(
            (capacity * abs(math.log(error_rate))) /
            (num_slices * (math.log(2) ** 2))))
        self._setup(error_rate, num_slices, bits_per_slice, capacity, 0)
        self.bitarray = bitarray.bitarray(self.num_bits, endian='little')
        self.bitarray.setall(False)

    def _setup(self, error_rate, num_slices, bits_per_slice, capacity, count):
        self.error_rate = error_rate
        self.num_slices = num_slices
        self.bits_per_slice = bits_per_slice
        self.capacity = capacity
        self.num_bits = num_slices * bits_per_slice
        self.count = count
        self.make_hashes = make_hashfuncs(self.num_slices, self.bits_per_slice)

    def __contains__(self, key):
        """Tests a key's membership in this bloom filter.
        >>> b = BloomFilter(capacity=100)
        >>> b.add("hello")
        False
        >>> "hello" in b
        True
        """
        bits_per_slice = self.bits_per_slice
        bitarray = self.bitarray
        hashes = self.make_hashes(key)
        offset = 0
        for k in hashes:
            if not bitarray[offset + k]:
                return False
            offset += bits_per_slice
        return True

計算公式基本一致。

算法將位圖分紅了k段(代碼中的num_slices,也就是哈希函數的數量k),每段長度爲代碼中的bits_per_slice,每一個哈希函數只負責將對應的段中的bit置爲1:

        for k in hashes:
            if not skip_check and found_all_bits and not bitarray[offset + k]:
                found_all_bits = False
            self.bitarray[offset + k] = True
            offset += bits_per_slice

當指望誤判率爲0.001時,m與n的比率大概是14:

>>> import math
>>> abs(math.log(0.001))/(math.log(2)**2)
14.37758756605116

當指望誤判率爲0.05時,m與n的比率大概是6:

>>> import math
>>> abs(math.log(0.05))/(math.log(2)**2)
6.235224229572683

上述題目中,m最大爲320億,n爲50億,誤判率大概爲0.04,在能夠接受的範圍:

>>> math.e**-((320/50.0)*(math.log(2)**2))
0.04619428041606246

應用

布隆過濾器通常用於在大數據量的集合中斷定某元素是否存在:

1. 緩存穿透

緩存穿透,是指查詢一個數據庫中不必定存在的數據。正常狀況下,查詢先進行緩存查詢,若是key不存在或者key已通過期,再對數據庫進行查詢,並將查詢到的對象放進緩存。若是每次都查詢一個數據庫中不存在的key,因爲緩存中沒有數據,每次都會去查詢數據庫,極可能會對數據庫形成影響。

緩存穿透的一種解決辦法是爲不存在的key緩存一個空值,直接在緩存層返回。這樣作的弊端就是緩存太多空值佔用了太多額外的空間,這點能夠經過給緩存層空值設立一個較短的過時時間來解決。

另外一種解決辦法就是使用布隆過濾器,查詢一個key時,先使用布隆過濾器進行過濾,若是判斷請求查詢key值存在,則繼續查詢數據庫;若是判斷請求查詢不存在,直接丟棄。

2. 爬蟲

在網絡爬蟲中,用於URL去重策略。

3. 垃圾郵件地址過濾

因爲垃圾郵件發送者能夠不停地註冊新地址,垃圾郵件的Email地址是一個巨量的集合。使用哈希表存貯幾十億個郵件地址可能須要上百GB的內存,而布隆過濾器只須要哈希表1/8到1/4的大小就能解決問題。布隆過濾器決不會漏掉任何一個在黑名單中的可疑地址。至於誤判問題,常見的補救辦法是在創建一個小的白名單,存儲那些可能被誤判的清白郵件地址。

4. Google的BigTable

Google的BigTable也使用了布隆過濾器,以減小不存在的行或列在磁盤上的I/O。

5. Summary Cache

Summary Cache是一種用於代理服務器Proxy之間共享Cache的協議。可使用布隆過濾器構建Summary Cache,每個Cache的網頁由URL惟一標識,所以Proxy的Cache內容能夠表示爲一個URL列表。進而咱們能夠將URL列表這個集合用布隆過濾器表示。

擴展

要實現刪除元素,能夠採用Counting Bloom Filter。它將標準布隆過濾器位圖的每一位擴展爲一個小的計數器(Counter),插入元素時將對應的k個Counter的值分別加1,刪除元素時則分別減1:

代價就是多了幾倍的存儲空間。

相關文章
相關標籤/搜索