布隆過濾器 原理 應用場景推導及Go實現

Bloom Filter(布隆過濾器)

布隆過濾器是一種多哈希函數映射的快速查找算法,通常應用在一些需要快速判斷某個元素是否屬於集合,但並不嚴格要求100%正確的場合。

Bloom算法類似一個hash set,用來判斷某個元素(key)是否在某個集合中。
和一般的hash set不同的是,這個算法無需存儲key的值,對於每個key,只需要k個比特位,每個存儲一個標誌,用來判斷key是否在集合中。
布隆過濾器可能會出現誤判,但不會漏判。即,如果過濾器判斷該元素不在集合中,則元素一定不在集合中,但如果過濾器判斷該元素在集合中,有一定的概率判斷錯誤(在合適的參數情況下,誤判率可以降低到0.000級別甚至更低)。

因此,Bloom Filter不適合那些“零錯誤”的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter相比於其他常見的算法極大節省了空間(相較於直接存儲,可節省上千倍的空間)。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是存在誤識別率和刪除困難。

優點:

相比於其它的數據結構,布隆過濾器在空間和時間方面都有巨大的優勢。布隆過濾器存儲空間和插入/查詢時間都是常數。另外, Hash 函數相互之間沒有關係,方便由硬件並行實現。布隆過濾器不需要存儲元素本身,在某些對保密要求非常嚴格的場合有優勢。
布隆過濾器可以表示全集,其它任何數據結構都不能;
k 和 m 相同,使用同一組 Hash 函數的兩個布隆過濾器的交併差運算可以使用位操作進行。

  1. 不需要存儲key,節省空間
缺點:

但是布隆過濾器的缺點和優點一樣明顯。誤算率(False Positive)是其中之一。隨着存入的元素數量增加,誤算率隨之增加。但是如果元素數量太少,則使用散列表足矣。
另外,一般情況下不能從布隆過濾器中刪除元素. 我們很容易想到把位列陣變成整數數組,每插入一個元素相應的計數器加1, 這樣刪除元素時將計數器減掉就可以了。然而要保證安全的刪除元素並非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器裏面. 這一點單憑這個過濾器是無法保證的。另外計數器迴繞也會造成問題。

  1. 算法判斷key在集合中時,有一定的概率key其實不在集合中
  2. 無法刪除
典型的應用場景:

某些存儲系統的設計中,會存在空查詢缺陷:當查詢一個不存在的key時,需要訪問慢設備,導致效率低下。
比如一個前端頁面的緩存系統,可能這樣設計:先查詢某個頁面在本地是否存在,如果存在就直接返回,如果不存在,就從後端獲取。但是當頻繁從緩存系統查詢一個頁面時,緩存系統將會頻繁請求後端,把壓力導入後端。

這是隻要增加一個bloom算法的服務,後端插入一個key時,在這個服務中設置一次
需要查詢後端時,先判斷key在後端是否存在,這樣就能避免後端的壓力。

  1. 網頁黑名單
  2. 垃圾郵件過濾
  3. 電話黑名單
  4. url去重
  5. Redis緩存穿透
  6. 比特幣錢包查詢
  7. 內容推薦等
算法步驟:
  1. 首先需要k個hash函數,每個函數可以把key散列成爲1個整數
  2. 初始化時,需要一個長度爲n比特的數組,每個比特位初始化爲0
  3. 某個key加入集合時,用k個hash函數計算出k個散列值,並把數組中對應的比特位置爲1
  4. 判斷某個key是否在集合時,用k個hash函數計算出k個散列值,並查詢數組中對應的比特位,如果所有的比特位都是1,認爲在集合中。

首先需要用到 bitset? 和計算hash 的2個庫。

import(
	"github.com/willf/bitset"	//bitset
	"github.com/spaolacci/murmur3" //hash計算
	"math"
)

哈希函數 - Murmur hash3

murmur hash是一種非加密型哈希函數,適用於一般的哈希檢索操作。對於規律性較強的key,murmurhash的隨機分佈特徵表現更良好。相比於md5,murmur hash在萬次測試中,性能高4-5倍。
在這裏插入圖片描述

下面定義布隆 過濾器的基礎結構。

type BloomFilter struct {
	m uint			//布隆過濾器key的個數
	k uint			//hash函數的個數
	b *bitset.BitSet //所有value
}
//比大小
func max(x,y uint)uint{
	if x > y {
		return x
	}
	return y
}

新建一個布隆 過濾器,mSet 就是 一個存儲 10101101 的數組 而 要判斷一個鍵,存在與否就是 找到散列在這個mSet上的值是否都設置爲了1,但現實中的數據分佈肯恩個會非常不均勻導致有些 位的重複率特別高 這樣會影響,查詢尋得準確率導致一些沒有的鍵也能被查到,所以需要用到散列函數,將數字或者文本散列在一個列表各個bit上,同時由於文本不能存儲在bitset中,所以hash函數還能幫助文本進行 去重。
在這裏插入圖片描述

func NewBloomFilter(m uint,k uint) *BloomFilter{
	return &BloomFilter{
		m: max(1,m), //最小1個
		k: max(1,k),//同理
		b: bitset.New(m),//bitmap
	}
}

根據數據量新建布隆過濾器,每多加一個數 容量就會添加64.

func From(data []uint,k uint)*BloomFilter{
	m := uint(len(data)*64)
	return &BloomFilter{
		m: m,
		k: k,
		b: bitset.From(data),
	}
}

在這裏插入圖片描述
如上圖 存放一個字符串 先將字符串hash 成 k = 3 個 hash整數 存在了 HashSet中。這所以用多個Hash 確認一個Key主要是可以減少誤判(不存在的Key 判斷爲了存在)的概率。

給定 數據 返回四個 hash的整數

func bashHashes(data []byte) [4]uint64{
	X := []byte{1}
	haser := murmur3.New128() //128的hash 2^128
	haser.Write(data)	//2個整數 1578830625359084202 6865968778589710305
	v1,v2 := haser.Sum128()
	haser.Write(X)
	v3,v4 := haser.Sum128() // 2個整數 
	return [4]uint64{v1,v2,v3,v4}	//返回四個整數
}

下面就是 輸入上面生成的四個隨機hash整數,然後通過運算可以生成K個整數,用於散列在bitset上。

func (f *BloomFilter) location(h [4]uint64,i uint) uint{
	//對計算處的位置 求餘 找到 布隆過濾器中的位置
	return uint(location(h,i) % uint64(f.m))
}
//
func location(h [4]uint64,i uint) uint64{
	ii := uint64(i)
	//hash 這個公式 是將 4 個 uint64 和 i 進行計算  傳入不同的i 可以算出不同的Hash i是 0 ~ k 個 。
	return h[ii %2] + ii*h[2+(((ii+(ii % 2))%4)/2)]
}
重複出現的概率

布隆過濾器出現誤判的機率和 他的大小是有關的,假設 布隆過濾器 有 m個元素,n個元素,每個元素散列 k個信息指紋的哈希函數。 每一個 bit位 被置爲 1的概率p(a)是一樣的:P(a)=1mP(a) = \frac{1}{m},所以每一位上不爲0的概率爲 1p(a)=11m1 - p(a) = 1 - \frac{1}{m} 那麼對於散列在HashSet上確定一個key的 K個散列值,不爲0的概率爲(11m)k(1 - \frac{1}{m})^k,假設第二個鍵插入到hashSet中,不重複的概率爲(11m)k(11m)k=(11m)2k(1 - \frac{1}{m})^k · (1 - \frac{1}{m})^k = (1 - \frac{1}{m})^{2k} 同理可得,
如果有N個不同的鍵通過K個散列Hash插入到大小爲M的hashSet中某個位不重複的概率爲:(11m)kn(1 - \frac{1}{m})^{kn},
再取反,在插入了N的Key後某一位不重複的概率爲:1(11m)kn1-(1- \frac{1}{m})^{kn},
假設來了一個新的Key 要讓他誤判成Key已經存在,相當於 要讓k位hash散列Bit都在已有的表中出現,而某一位重複出現的概率就是上面的公式,那麼K位重複的概率就是:(1(11m)kn)k(1eknm)k(1-(1- \frac{1}{m})^{kn})^{k} ≈ (1 - e^{\frac{kn}{m}})^k,

推理過程爲,當n比較大時,

引入一個我們很熟悉的公式:(11m)me(1 - \frac{1}{m})^{-m} ≈ e 大學都應該有學過。

所以可以變形爲:

(1eknm)k(1 - e ^{\frac{-kn}{m}})^k

接下來 假設錯誤率p(e)=(1eknm)kp(e) = (1 - e ^{\frac{-kn}{m}})^k

對公示兩邊取自然對數:

lnp=kln(1eknm)lnp = kln(1 - e ^{\frac{-kn}{m}})

然後再進行求導:

=kln(1eknm)k= \frac{\partial{kln}(1 - e ^{\frac{-kn}{m}})}{\partial{k}} =ln(1eknm)+knmeknm1eknmln(1 - e^{\frac{-kn}{m}}) + k\frac{\frac{n}{m}e^{\frac{-kn}{m}}}{1 - e^{\frac{-kn}{m}}}
lnpk=ln(1x)x1x\frac{\partial{lnp}}{\partial{k}} = ln(1 - x) - \frac{x}{1-x}
x=eknmx = e ^{\frac{-kn}{m}} $則 k=mnlnxk = \frac{-m}{n}lnx
得 : lnpk=ln(1x)x1xlnx\frac{\partial{lnp}}{\partial{k}} = ln(1 -x) - \frac{x} {1-x} * lnx

lnpk\frac{\partial{lnp}}{\partial{k}} 取極限0,x=12x = \frac{1}{2} 最後解得 k 爲: mnln2\frac{m}{n}ln2

當hash函數個數 k = ln2 * m / n 時錯誤率最小。

以下是 不同m/n 情況下 不同多個k 出現誤判的機率:
在這裏插入圖片描述
布隆過濾器論文

對於給定 錯誤率 p,如何選擇最優的位數的數組大小呢?
m=nlnp(ln2)2m = -\frac{nlnp}{(ln2)^2}
用來計算最合適的k個hash k=mnln2k=\frac{m}{n}ln2

假設 有 10 億數據 錯誤率 0.01% 那要多少空間呢?

讓我們來算一算 下面是python代碼:

import math
n = 1000000000
p = 0.0001
m = -(n *math.log(p,math.e)/(math.log(2,math.e ) ** 2))
k = m/n * math.log(2,math.e ) 
m = m / 8 / 1024 /1024 / 1024

在這裏插入圖片描述
計算結果爲 M爲 2 .2 個G 左右 K 爲 13 。這種情況下 10億個數據也就會 誤判1萬個左右而且只用了 2個G 還是可以接受的。

下面用Go實現這兩個公式,用來估計假陽性(不存在但查出來是存在)的概率和 用k個Hash 來存儲一個key k的取值最合適的值。

func EStamatewithParameters(n uint,p float64) (m uint,k uint){
	m = uint(math.Ceil( -1 * float64(n) * math.Log(p)) / math.Pow(math.Log(2),2))
	k = uint(math.Ceil(math.Log(2) * float64(m)/float64(n)))
	return m,k
}
//得到 多少蛇值多少個hash
func (f *BloomFilter)K()uint{
	return f.k //hash
}

//包含的key大小
func (f *BloomFilter)Cap()uint{
	return f.m//布隆過濾器存放的數據量
}
func (f *BloomFilter)Add(data []byte)*BloomFilter{
	h := bashHashes(data)//計算哈希
	for i:= uint(0);i<f.k;i++{
		f.b.Set(f.location(h,i)) //設置數據
	}
	return f
}


//新建一個布隆過濾器,預估數據規模
func NewwithEstimates(n uint,p float64) *BloomFilter{
	m,k := EStamatewithParameters(n,p)
	return NewBloomFilter(m,k)
}

下面的代碼 通過循環K次,通過f.location這個Hash計算函數計算出 f.k 個位置,設置到 bitset中。

func (f *BloomFilter)Add(data []byte)*BloomFilter{
	//生成hash
	h := bashHashes(data)
	//循環k 次生成hash
	for i:= uint(0);i<f.k;i++{
		//把hash函數映射到對應的位置
		f.b.Set(f.location(h,i))
	}
	return f
}

合併 就是 把 2 個 bitSet 合在一起,其實也就是執行或操作。
在這裏插入圖片描述

func (f *BloomFilter) Merge(g *BloomFilter)error{
	if f.m != g.m{
		return fmt.Errorf("大小不一樣!")
	}
	if f.k != g.k{
		return fmt.Errorf("Key不一樣!")
	}
	f.b.InPlaceUnion(g.b) //歸併 bitset
	return nil
}

完整版:

type BloomFilter struct {
	m uint //容量
	k uint //hash函數個數
	b *bitset.BitSet
}

func max(x,y uint)uint{
	if x > y {
		return x
	}
	return y
}
//新建一個布隆 過濾器
func NewBloomFilter(m uint,k uint) *BloomFilter{
	return &BloomFilter{
		m: max(1,m),
		k: max(1,k),
		b: bitset.New(m),
	}
}
//根據數據量新建布隆過濾器
func From(data []uint64,k uint)*BloomFilter{
	m := uint(len(data)*64)
	return &BloomFilter{
		m: m,
		k: k,
		b: bitset.From(data),
	}
}
//對字符串進行hash
func bashHashes(data []byte) [4]uint64{
	X := []byte{1}
	haser := murmur3.New128() //128的hash 2^128
	haser.Write(data)
	v1,v2 := haser.Sum128()
	haser.Write(X)
	v3,v4 := haser.Sum128()
	return [4]uint64{v1,v2,v3,v4}
}

func location(h [4]uint64,i uint) uint64{
	ii := uint64(i)
	return h[ii %2] + ii*h[2+(((ii+(ii % 2))%4)/2)]
}

func (f *BloomFilter) location(h [4]uint64,i uint) uint{
	return uint(location(h,i) % uint64(f.m))
}

func EStamatewithParameters(n uint,p float64) (m uint,k uint){
	m = uint(math.Ceil( -1 * float64(n) * math.Log(p)) / math.Pow(math.Log(2),2))
	k = uint(math.Ceil(math.Log(2) * float64(m)/float64(n)))
	return m,k
}


//新建一個布隆過濾器,預估數據規模
func NewwithEstimates(n uint,p float64) *BloomFilter{
	m,k := EStamatewithParameters(n,p)
	return NewBloomFilter(m,k)
}

func (f *BloomFilter)K()uint{
	return f.k //hash
}

func (f *BloomFilter)Cap()uint{
	return f.m//數量
}

func (f *BloomFilter)Add(data []byte)*BloomFilter{
	h := bashHashes(data)
	for i:= uint(0);i<f.k;i++{
		f.b.Set(f.location(h,i))
	}
	return f
}

func (f *BloomFilter) Merge(g *BloomFilter)error{
	if f.m != g.m{
		return fmt.Errorf("大小不一樣!")
	}
	if f.k != g.k{
		return fmt.Errorf("Key不一樣!")
	}
	f.b.InPlaceUnion(g.b) //歸併 bitset
	return nil
}
//拷貝新建一個布隆過濾器
func (f *BloomFilter)Copy() *BloomFilter{
	fc := NewBloomFilter(f.m,f.k)
	fc.Merge(f)
	return fc
}

func (f *BloomFilter)AddString(data string)*BloomFilter{
	return f.Add([]byte(data))
}

func (f *BloomFilter)Test(data []byte) bool{
	h := bashHashes(data)
	for i:= uint(0);i < f.k;i++{
		if !f.b.Test(f.location(h,i)){
			return false
		}
	}
	return true
}
func (f *BloomFilter)TestString(data []byte) bool{
	return f.Test([]byte(data))
}
//測試整數 是否存在
func (f *BloomFilter)TestLocations(locs []uint64) bool{
	for i := 0;i<len(locs);i++{
		if !f.b.Test(uint(locs[i] % uint64(f.m))){
			return false
		}
	}
	return true
}


//測試是否存在,存在就更新
func (f *BloomFilter)TestAndAdd(data []byte) bool{
	isin := true
	h := bashHashes(data)
	for i := uint(0);i<f.k;i++{
		if !f.b.Test(f.location(h,i)){
			isin = false
		}
		f.b.Set(1)
	}
	return isin
}

//布隆過濾器的
type BloomFilterJson struct{
	M uint  `json:"m"`
	K uint  `json:"k"`
	B *bitset.BitSet  `json:"b"`
}
//字節轉對象
func (f *BloomFilter)MarshaJson() ([]byte,error){
	return json.Marshal(BloomFilterJson{f.m,f.k,f.b})
}
//字節轉對象
func (f *BloomFilter)UnMarshaJson(data []byte) (error){
	var j BloomFilterJson
	err := json.Unmarshal(data,&j)
	if err != nil{
		return err
	}
	f.m = j.M
	f.k = j.K
	f.b = j.B
	return nil
}
func (f *BloomFilter)Writeto(stream io.Writer)(int64,error){
	err := binary.Write(stream,binary.BigEndian,uint64(f.m))
	if err != nil{
		return 0,err
	}
	err = binary.Write(stream,binary.BigEndian,uint64(f.k))
	if err != nil{
		return 0,err
	}
	bumbytes ,err := f.b.WriteTo(stream)
	return bumbytes + int64(2 * binary.Size(uint64(0))),err
}
func (f *BloomFilter)Readfrom(stream io.Reader)(int64,error){
	var m,k uint64
	err := binary.Read(stream,binary.BigEndian,&m)
	if err != nil{
		return 0,err
	}
	err = binary.Read(stream,binary.BigEndian,&k)
	if err != nil{
		return 0,err
	}
	b := &bitset.BitSet{}
	f.m = uint(m)
	f.k = uint(k)
	f.b = b
	numbyte,err := b.ReadFrom(stream)
	return numbyte + int64(2 *binary.Size(uint64(0))),err
}

func (f *BloomFilter) GoDecode(data []byte) (error){
	buf := bytes.NewBuffer(data)
	_,err := f.Readfrom(buf)
	return err
}
func (f *BloomFilter) GoEncode() ([]byte,error){
	var buf bytes.Buffer
	_,err := f.Writeto(&buf)
	if err != nil{
		return nil,err
	}
	return buf.Bytes(),nil
}
func locations(data []byte,k uint) []uint64{
	locs := make([]uint64,k)
	h := bashHashes(data)
	for i:= uint(0);i<k;i++{
		locs[i] = location(h,i)
	}
	return locs
}


//判斷布隆過濾器是否相等
func (f *BloomFilter) Equal (g *BloomFilter) bool{
	return f.m == g.m && f.k == g.k && f.b.Equal(g.b)
}


//測試是否存在,存在就更新
func (f *BloomFilter)TestAndAddString(data []byte) bool{
	return f.TestAndAdd([]byte(data))
}
//清空布隆過濾器
func (f *BloomFilter)Clear() *BloomFilter{
	f.b.ClearAll()
	return  f
}
//測試正確率
func (f*BloomFilter)EstimateFalsePositiveRate(n uint,rounds uint32)(fpRate float64){
	f.Clear()
	n1:=make([]byte,4)//開闢字節數組
	for i:=uint32(0);i<uint32(n);i++{ 
		binary.BigEndian.PutUint32(n1,i)
		f.Add(n1)//循環n次生成字節加入 布隆過濾器
	}
	fp:=0
	for i:=uint32(0);i<rounds;i++{ //這裏生成 數據測試 是不是在布隆過濾器裏
		binary.BigEndian.PutUint32(n1,i+uint32(n)+1)
		if f.Test(n1) {
			fp++
		}

	}
	fpRate=float64(fp)/float64(rounds)//正確率
	f.Clear()
	return


}

測試代碼:

f:=NewwithEstimates(100000000,0.001) //計算 1億個數據 錯誤率爲 0.001的情況下 m 和 k的值 並創建布隆過濾器
n1:=[]byte("123123")
n2:=[]byte("sdfsdf")
n3:=[]byte("dsfsdf")
n4:=[]byte("caomaoboy7777")
n5:=[]byte("helloworld")
f.Add(n1)
f.Add(n2)
f.Add(n3)
//這裏測試序列化
data,_ := f.MarshaJson()
var g BloomFilter
err := g.UnMarshaJson(data)
if err != nil{
	fmt.Println("error!")
}
fmt.Println(f.Test(n1))
fmt.Println(f.Test(n3))
fmt.Println(f.Test(n4))
fmt.Println(f.Test(n5))
fmt.Println(f.Test(n3))
f.Clear()
fmt.Println(f.EstimateFalsePositiveRate(100000000)) //測試 插入100000000  如果正確的話 會輸出 0.001左右的值

---------

在這裏插入圖片描述
和設置的正確率是一樣的說明代碼沒什麼問題。

參考:
吳軍 – 《數學之美》
詳解布隆過濾器(Bloomfilter)+scrapy分佈式持久化去重 ----https://www.jianshu.com/p/e4773b69319d
詳解布隆過濾器的原理、使用場景和注意事項 ----- https://www.jianshu.com/p/2104d11ee0a2
[算法系列之十]大數據量處理利器:布隆過濾器 -------https://yq.aliyun.com/articles/3607
Google算法工程師尹成帶你深度學習數據結構與算法導論-----https://edu.51cto.com/course/20394.html?source=so

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章