Bloom Filter(布隆過濾器)
布隆過濾器是一種多哈希函數映射的快速查找算法,通常應用在一些需要快速判斷某個元素是否屬於集合,但並不嚴格要求100%正確的場合。
Bloom算法類似一個hash set,用來判斷某個元素(key)是否在某個集合中。
和一般的hash set不同的是,這個算法無需存儲key的值,對於每個key,只需要k個比特位,每個存儲一個標誌,用來判斷key是否在集合中。
布隆過濾器可能會出現誤判,但不會漏判。即,如果過濾器判斷該元素不在集合中,則元素一定不在集合中,但如果過濾器判斷該元素在集合中,有一定的概率判斷錯誤(在合適的參數情況下,誤判率可以降低到0.000級別甚至更低)。
因此,Bloom Filter不適合那些“零錯誤”的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter相比於其他常見的算法極大節省了空間(相較於直接存儲,可節省上千倍的空間)。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是存在誤識別率和刪除困難。
優點:
相比於其它的數據結構,布隆過濾器在空間和時間方面都有巨大的優勢。布隆過濾器存儲空間和插入/查詢時間都是常數。另外, Hash 函數相互之間沒有關係,方便由硬件並行實現。布隆過濾器不需要存儲元素本身,在某些對保密要求非常嚴格的場合有優勢。
布隆過濾器可以表示全集,其它任何數據結構都不能;
k 和 m 相同,使用同一組 Hash 函數的兩個布隆過濾器的交併差運算可以使用位操作進行。
- 不需要存儲key,節省空間
缺點:
但是布隆過濾器的缺點和優點一樣明顯。誤算率(False Positive)是其中之一。隨着存入的元素數量增加,誤算率隨之增加。但是如果元素數量太少,則使用散列表足矣。
另外,一般情況下不能從布隆過濾器中刪除元素. 我們很容易想到把位列陣變成整數數組,每插入一個元素相應的計數器加1, 這樣刪除元素時將計數器減掉就可以了。然而要保證安全的刪除元素並非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器裏面. 這一點單憑這個過濾器是無法保證的。另外計數器迴繞也會造成問題。
- 算法判斷key在集合中時,有一定的概率key其實不在集合中
- 無法刪除
典型的應用場景:
某些存儲系統的設計中,會存在空查詢缺陷:當查詢一個不存在的key時,需要訪問慢設備,導致效率低下。
比如一個前端頁面的緩存系統,可能這樣設計:先查詢某個頁面在本地是否存在,如果存在就直接返回,如果不存在,就從後端獲取。但是當頻繁從緩存系統查詢一個頁面時,緩存系統將會頻繁請求後端,把壓力導入後端。
這是隻要增加一個bloom算法的服務,後端插入一個key時,在這個服務中設置一次
需要查詢後端時,先判斷key在後端是否存在,這樣就能避免後端的壓力。
- 網頁黑名單
- 垃圾郵件過濾
- 電話黑名單
- url去重
- Redis緩存穿透
- 比特幣錢包查詢
- 內容推薦等
算法步驟:
- 首先需要k個hash函數,每個函數可以把key散列成爲1個整數
- 初始化時,需要一個長度爲n比特的數組,每個比特位初始化爲0
- 某個key加入集合時,用k個hash函數計算出k個散列值,並把數組中對應的比特位置爲1
- 判斷某個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)是一樣的:,所以每一位上不爲0的概率爲 那麼對於散列在HashSet上確定一個key的 K個散列值,不爲0的概率爲,假設第二個鍵插入到hashSet中,不重複的概率爲 同理可得,
如果有N個不同的鍵通過K個散列Hash插入到大小爲M的hashSet中某個位不重複的概率爲:,
再取反,在插入了N的Key後某一位不重複的概率爲:,
假設來了一個新的Key 要讓他誤判成Key已經存在,相當於 要讓k位hash散列Bit都在已有的表中出現,而某一位重複出現的概率就是上面的公式,那麼K位重複的概率就是:,
推理過程爲,當n比較大時,
引入一個我們很熟悉的公式: 大學都應該有學過。
所以可以變形爲:
。
接下來 假設錯誤率
對公示兩邊取自然對數:
然後再進行求導:
=
設 $則
得 :
對 取極限0, 最後解得 k 爲:
當hash函數個數 k = ln2 * m / n 時錯誤率最小。
以下是 不同m/n 情況下 不同多個k 出現誤判的機率:
布隆過濾器論文
對於給定 錯誤率 p,如何選擇最優的位數的數組大小呢?
用來計算最合適的k個hash
假設 有 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