先從一道面試題開始: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)是1970年由Bloom提出的。它能夠用於檢索一個元素是否在一個集合中。服務器
布隆過濾器實際上是位圖的一種擴展,不一樣的是要使用多個哈希函數。它包括一個很長的二進制向量(位圖)和一系列隨機映射函數。網絡
首先創建一個m位的位圖,而後對於每個加入的元素,使用k個哈希函數求k個哈希值映射到位圖的k個位置,而後將這k個位置的bit全設置爲1。下圖是k=3的布隆過濾器:函數
檢索時,咱們只要檢索這些k個位是否是都是1就能夠了:若是這些位有任何一個0,則被檢元素必定不在;若是都是1,則被檢元素極可能在。
能夠看出布隆過濾器在時間和空間上的效率比較高,但也有缺點:
要實現一個布隆過濾器,咱們須要預估要存儲的數據量爲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:
代價就是多了幾倍的存儲空間。