Redis 高級主題之布隆過濾器(BloomFilter)

最近計劃準備整理幾篇關於Reids高級主題的博文,本文整理的是關於布隆過濾器在Redis中如何應用,先來一張思惟導圖瀏覽全文。python

1. 認識BloomFilter

1.1 原理

布隆過濾器,英文叫BloomFilter,能夠說是一個二進制向量和一系列隨機映射函數實現。 能夠用於檢索一個元素是否在一個集合中。git

下面來看看布隆過濾器是如何判斷元素在一個集合中,以下圖:github

有三個hash函數和一個位數組,oracle通過三個hash函數,獲得第一、四、5位爲1,database同理獲得二、五、10位1,這樣若是咱們須要判斷oracle是否在此位數組中,則經過hash函數判斷位數組的一、四、5位是否均爲1,若是均爲1,則判斷oracle在此位數組中,database同理。這就是布隆過濾器判斷元素是否在集合中的原理。redis

想必聰明的讀者已經發現,若是bloom通過三個hash算法,須要判斷 一、五、10位是否爲1,剛好由於位數組中添加oracle和database致使一、五、10爲1,則布隆過濾器會判斷bloom會判斷在集合中,這不是Bug嗎,致使誤判。可是能夠保證的是,若是布隆過濾器判斷一個元素不在一個集合中,那這個元素必定不會再集合中。算法

是的,這個是布隆過濾器的缺點,有一點的誤識別率,可是布隆過濾器有2大優勢,使得這個缺點在某些應用場景中是能夠接受的,2大優勢是空間效率和查詢時間都遠遠高於通常的算法。常規的數據結構set,也是通過被用於判斷一個元素是否在集合中,但若是有上百萬甚至更高的數據,set結構所佔的空間將是巨大的,布隆過濾器只須要上百萬個位便可,10多Kb便可。sql

致使這個缺點是由於hash碰撞,但布隆過濾器經過多個hash函數來下降hash碰撞帶來的誤判率,以下圖:docker

當只有1個hash函數的時候,誤判率很高,但4個hash函數的時候已經縮小10多倍,能夠動態根據業務需求所容許的識別率來調整hash函數的個數,固然hash函數越多,所帶來的空間效率和查詢效率也會有所下降。數據庫

第二個缺點相對set來講,不能夠刪除,由於布隆過濾器並無存儲key,而是經過key映射到位數組中。數組

總結,敲黑板:緩存

  • 布隆過濾器是用於判斷一個元素是否在集合中。經過一個位數組和N個hash函數實現。
  • 優勢:
    • 空間效率高,所佔空間小。
    • 查詢時間短。
  • 缺點:
    • 元素添加到集合中後,不能被刪除。
    • 有必定的誤判率

1.2 應用場景

  • 數據庫防止穿庫。 Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter來減小不存在的行或列的磁盤查找。避免代價高昂的磁盤查找會大大提升數據庫查詢操做的性能。
  • 業務場景中判斷用戶是否閱讀過某視頻或文章,好比抖音或頭條,固然會致使必定的誤判,但不會讓用戶看到重複的內容。還有以前本身遇到的一個比賽類的社交場景中,須要判斷用戶是否在比賽中,若是在則須要更新比賽內容,也可使用布隆過濾器,能夠減小不在的用戶查詢db或緩存的次數。
  • 緩存宕機、緩存擊穿場景,通常判斷用戶是否在緩存中,若是在則直接返回結果,不在則查詢db,若是來一波冷數據,會致使緩存大量擊穿,形成雪崩效應,這時候能夠用布隆過濾器當緩存的索引,只有在布隆過濾器中,纔去查詢緩存,若是沒查詢到,則穿透到db。若是不在布隆器中,則直接返回。
  • WEB攔截器,若是相同請求則攔截,防止重複被攻擊。用戶第一次請求,將請求參數放入布隆過濾器中,當第二次請求時,先判斷請求參數是否被布隆過濾器命中。能夠提升緩存命中率。

1.3 動手實現布隆過濾器

根據布隆過濾器的原理,來用 Python 手動實現一個布隆過濾器。 首先須要安裝 mmh3,mmh3是 MurmurHash3 算法的實現,Redis 中也是採用此hash算法。而後還須要安裝 bitarray,Python 中位數組的實現。

pip install mmh3
pip install bitarray
複製代碼

準備好環境後,開始實現布隆過濾器,直接上代碼

# python 3.6 simple_bloomfilter.py
import mmh3
from bitarray import bitarray

class BloomFilter(object):
    def __init__(self, bit_size=10000, hash_count=3, start_seed=41):
        self.bit_size = bit_size
        self.hash_count = hash_count
        self.start_seed = start_seed
        self.initialize()

    def initialize(self):
        self.bit_array = bitarray(self.bit_size)
        self.bit_array.setall(0)

    def add(self, data):
        bit_points = self.get_hash_points(data)
        for index in bit_points:
            self.bit_array[index] = 1

    def is_contain(self, data):
        bit_points = self.get_hash_points(data)
        result = [self.bit_array[index] for index in bit_points]
        return all(result)

    def get_hash_points(self, data):
        return [
            mmh3.hash(data, index) % self.bit_size
            for index in range(self.start_seed, self.start_seed +
                               self.hash_count)
        ]
複製代碼

完整代碼均記錄在github.com/fuzctc/tc-r…

上述代碼中實現了BloomFilter類,初始化三個變量,bit_size 位數組的大小,hash_count Hash函數的數量,start_seed 起始hash隨機種子數。

實現了2個方法,add 方法是根據data通過多個hash函數獲得不一樣的bit位,將位數組中相關位置1,is_contain是判斷data是否在BloomFilter中。

測試一下代碼:

>>> from simple_bloomfilter import BloomFilter
>>> bloomFilter = BloomFilter(1000000, 6)
>>> bloomFilter.add('databases')
>>> bloomFilter.add('bloomfilter')
>>> bloomFilter.is_contain('databases')
True
>>> bloomFilter.is_contain('bloomfilter')
True
>>> bloomFilter.is_contain('bloomfilte')
False
複製代碼

測試BloomFilter功能有效,可是實際在生產過程當中,存儲的量級很是大,一般採用redis的bitmap數據結構來代替本身實現的位數組,下面來實踐一下在Redis中如何使用布隆過濾器。

2. Redis-BloomFilter實踐

Redis在4.0版本推出了 module 的形式,能夠將 module 做爲插件額外實現Redis的一些功能。官網推薦了一個 RedisBloom 做爲 Redis 布隆過濾器的 Module。

除了這個還有別的方式能夠實現,下面一一列舉一下:

下面一一來實踐一下。

2.1 RedisBloom

RedisBloom須要先進行安裝,推薦使用Docker進行安裝,簡單方便:

docker pull redislabs/rebloom:latest
docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest
docker exec -it redis-redisbloom bash
# redis-cli
# 127.0.0.1:6379> bf.add tiancheng hello
複製代碼

固然也能夠直接編譯進行安裝:

git clone https://github.com/RedisBloom/RedisBloom.git
cd RedisBloom
make //編譯 會生成一個rebloom.so文件
redis-server --loadmodule /path/to/rebloom.so
redis-cli -h 127.0.0.1 -p 6379
複製代碼

此模塊不只僅實現了布隆過濾器,還實現了 CuckooFilter(布穀鳥過濾器),以及 TopK 功能。CuckooFilter 是在 BloomFilter 的基礎上主要解決了BloomFilter不能刪除的缺點。先來看看 BloomFilter,後面介紹一下 CuckooFilter。

2.1.1 BloomFilter 相關操做

先來熟悉一下布隆過濾器基本指令:

  • bf.add 添加元素到布隆過濾器
  • bf.exists 判斷元素是否在布隆過濾器
  • bf.madd 添加多個元素到布隆過濾器,bf.add只能添加一個
  • bf.mexists 判斷多個元素是否在布隆過濾器
127.0.0.1:6379> bf.add tiancheng tc01
(integer) 1
127.0.0.1:6379> bf.add tiancheng tc02
(integer) 1
127.0.0.1:6379> bf.add tiancheng tc03
(integer) 1
127.0.0.1:6379> bf.exists tiancheng tc01
(integer) 1
127.0.0.1:6379> bf.exists tiancheng tc02
(integer) 1
127.0.0.1:6379> bf.exists tiancheng tc03
(integer) 1
127.0.0.1:6379> bf.exists tiancheng tc04
(integer) 0
127.0.0.1:6379> bf.madd tiancheng tc05 tc06 tc07
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists tiancheng tc05 tc06 tc07 tc08
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0
複製代碼

2.1.2 測試誤判率

接下來來測試一下誤判率:

import redis
client = redis.StrictRedis()

client.delete("tiancheng")
size = 100000
count = 0
for i in range(size):
    client.execute_command("bf.add", "tiancheng", "tc%d" % i)
    result = client.execute_command("bf.exists", "tiancheng", "tc%d" % (i + 1))
    if result == 1:
        # print(i)
        count += 1

print("size: {} , error rate: {}%".format(
    size, round(count / size * 100, 5)))
複製代碼

測試結果以下:

➜  BloomFilter git:(master) ✗ python redisbloom_test.py
size: 1000 , error rate: 1.0%
➜  BloomFilter git:(master) ✗ python redisbloom_test.py
size: 10000 , error rate: 1.25%
➜  BloomFilter git:(master) ✗ python redisbloom_test.py
size: 100000 , error rate: 1.304%
複製代碼

size=1000,就出現1%的誤判率,size越高誤判率會越高,那有沒有辦法控制誤判率了,答案是有的。

實際上布隆過濾器是提供自定義參數,以前都是使用默認的參數,此模塊還提供了一個命令bf.reserve,提供了三個參數, key, error_rate和initial_size。錯誤率越低,須要的空間越大,initial_size參數表示預計放入布隆過濾器的元素數量,當實際數量超出這個數值時,誤判率會上升。 默認的參數是 error_rate=0.01, initial_size=100。

接下來測試一下:

import redis
client = redis.StrictRedis()

client.delete("tiancheng")
size = 10000
count = 0
client.execute_command("bf.reserve", "tiancheng", 0.001, size) # 新增
for i in range(size):
    client.execute_command("bf.add", "tiancheng", "tc%d" % i)
    result = client.execute_command("bf.exists", "tiancheng", "tc%d" % (i + 1))
    if result == 1:
        #print(i)
        count += 1

print("size: {} , error rate: {}%".format(
    size, round(count / size * 100, 5)))
複製代碼

新增一行代碼,簡單測試一下效果:

➜  BloomFilter git:(master) ✗ python redisbloom_test.py
size: 10000 , error rate: 0.0%
➜  BloomFilter git:(master) ✗ python redisbloom_test.py
size: 100000 , error rate: 0.001%
複製代碼

誤判率瞬間少了1000多倍。

可是要求誤判率越低,所須要的空間是須要越大,能夠有一個公式計算,因爲公式較複雜,直接上相似計算器,感覺一下:

若是一千萬的數據,誤判率容許 1%, 大概須要11M左右,以下圖:

若是要求誤判率爲 0.1%,則大概須要 17 M左右。

但這空間相比直接用set存1000萬數據要少太多了。

2.1.3 擴展學習

RedisBloom 模塊 還實現了 布穀鳥過濾器,簡單瞭解了一下,有一篇論文有興趣的通訊能夠讀一下 www.cs.cmu.edu/~dga/papers…

文章中對比了布隆過濾器和布穀鳥過濾器,相比布穀鳥過濾器,布隆過濾器有如下不足:

  • 查詢性能弱 查詢性能弱是由於布隆過濾器須要使用N個 hash函數計算位數組中N個不一樣的點,這些點在內存上跨度較大,會致使CPU緩存命中率低。
  • 空間利用效率低 在相同的誤判率下,布穀鳥的空間利用率要高於布隆過濾器,能節省40%左右。
  • 不支持刪除操做 這是布穀鳥過濾器相對布隆過濾器最大的優化,支持反向操做,刪除功能。
  • 不支持計數

因暫時未對布穀鳥過濾器進行比較深刻的瞭解,不清楚究竟是不是文章說的那麼好,有時間再研究一下。

2.2 pyreBloom

pyreBloom 是 Python 中 Redis + BloomFilter 模塊,是c語言實現。若是以爲Redis module的形式部署很麻煩或者線上環境Redis版本不是 4.0 及以上,則能夠採用這個,可是它是在 hiredis 基礎上,須要安裝hiredis,且不支持重連和重試,若是用到生產環境上須要進行簡單的封裝。

安裝:

git clone https://github.com/redis/hiredis.git src/hiredis && \
cd src/hiredis && make && make PREFIX=/usr install && ldconfig

// mac brew install hiredis
git clone https://github.com/seomoz/pyreBloom src/pyreBloom && \
cd src/pyreBloom && python setup.py install
複製代碼

演示代碼:

from pyreBloom import pyreBloom

redis_conf = {'host': '127.0.0.1', 'password': '', 'port': 6379, 'db': 0}

for k, v in redis_conf.items():
    redis_conf = convert_utf8(redis_conf)

key = convert_utf8('tc')
value = convert_utf8('hello')

p = pyreBloom(key, 10000, 0.001, **redis_conf)
p.add(value)
print(p.contains(value))
複製代碼

2.3 原生語言 + redis 實現

Python 原生語言比較慢,若是是Go語言,沒有找到合適的開源redis的BloomFilter,就能夠本身用原生語言 + redis的 bitmap 相關操做實現。 Java 語言的話,RedisBloom項目裏有實現 JReBloom,因非Java開發者,這塊能夠自行了解。

這裏演示用 Python 語言,Go 語言版本有時間再補充:

# python3.6 bloomfilter_py_test.py
import mmh3
import redis


class BloomFilter(object):
    def __init__(self, bf_key, bit_size=10000, hash_count=3, start_seed=41):
        self.bit_size = bit_size
        self.hash_count = hash_count
        self.start_seed = start_seed
        self.client = redis.StrictRedis()
        self.bf_key = bf_key

    def add(self, data):
        bit_points = self._get_hash_points(data)
        for index in bit_points:
            self.client.setbit(self.bf_key, index, 1)

    def madd(self, m_data):
        if isinstance(m_data, list):
            for data in m_data:
                self.add(data)
        else:
            self.add(m_data)

    def exists(self, data):
        bit_points = self._get_hash_points(data)
        result = [
            self.client.getbit(self.bf_key, index) for index in bit_points
        ]
        return all(result)

    def mexists(self, m_data):
        result = {}
        if isinstance(m_data, list):
            for data in m_data:
                result[data] = self.exists(data)
        else:
            result[m_data] = self.exists[m_data]
        return result

    def _get_hash_points(self, data):
        return [
            mmh3.hash(data, index) % self.bit_size
            for index in range(self.start_seed, self.start_seed +
                               self.hash_count)
        ]
複製代碼

在上文的simple_bloomfilter.py的基礎引入redis的setbitgetbit,替換掉 bitarray。同時實現了相似BloomFilter的 madd 和 mexists 方法,測試結果以下:

>>> from bloomfilter_py_test import BloomFilter
>>> bf_obj = BloomFilter('bf.tc')
>>> bf_obj.add('tc01')
>>> bf_obj.madd('tc02')

>>> bf_obj.madd(['tc03', 'tc04'])
>>> bf_obj.exists('tc01')
True
>>> bf_obj.mexists(['tc02', 'tc03', 'tc04', 'tc05'])
{'tc02': True, 'tc03': True, 'tc04': True, 'tc05': False}
複製代碼

布隆過濾器相關內容就實踐到這裏。相關代碼在github.com/fuzctc/tc-r…

更多Redis相關文章和討論,請關注公衆號:『 天澄技術雜談 』

相關文章
相關標籤/搜索