最近計劃準備整理幾篇關於Reids高級主題的博文,本文整理的是關於布隆過濾器在Redis中如何應用,先來一張思惟導圖瀏覽全文。python
布隆過濾器,英文叫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映射到位數組中。數組
總結,敲黑板:緩存
根據布隆過濾器的原理,來用 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中如何使用布隆過濾器。
Redis在4.0版本推出了 module 的形式,能夠將 module 做爲插件額外實現Redis的一些功能。官網推薦了一個 RedisBloom 做爲 Redis 布隆過濾器的 Module。
除了這個還有別的方式能夠實現,下面一一列舉一下:
下面一一來實踐一下。
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。
先來熟悉一下布隆過濾器基本指令:
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
複製代碼
接下來來測試一下誤判率:
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萬數據要少太多了。
RedisBloom 模塊 還實現了 布穀鳥過濾器,簡單瞭解了一下,有一篇論文有興趣的通訊能夠讀一下 www.cs.cmu.edu/~dga/papers…
文章中對比了布隆過濾器和布穀鳥過濾器,相比布穀鳥過濾器,布隆過濾器有如下不足:
因暫時未對布穀鳥過濾器進行比較深刻的瞭解,不清楚究竟是不是文章說的那麼好,有時間再研究一下。
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))
複製代碼
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的setbit
和 getbit
,替換掉 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相關文章和討論,請關注公衆號:『 天澄技術雜談 』