├── .gitignore ├── README.md ├── bloomfilter ├── __init__.py ├── base.py ├── mock.py ├── test_base.py ├── test_utils.py └── utils.py ├── doc ├── bloomfilter_in_action.md ├── bloomfilter_principle.md └── images │ ├── bloomfilter-cache-mysql.png │ ├── bloomfilter-collision-100.png │ ├── bloomfilter-collision-250.png │ ├── bloomfilter-collision.png │ ├── bloomfilter-filter.png │ ├── bloomfilter-more-hash.png │ ├── bloomfilter-one-hash.png │ └── bloomfilter-redis-cache-mysql.png ├── examples └── read.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | *.swp 107 | *.orig 108 | .DS_Store 109 | .project 110 | tags 111 | .idea 112 | .*un~ 113 | 114 | dist 115 | build 116 | bloomfilter.egg-info/ 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bloom filter 过滤器 2 | 高效的挡住无效的请求和流量,可用于数据库查询前,过滤非命中查询。也可以用来防止redis缓存击穿。 3 | 4 | ## WHY 5 | - [bloomfilter过滤器实战](doc/bloomfilter_in_action.md) 6 | - [bloomfilter的原理](doc/bloomfilter_principle.md) 7 | 8 | ## 安装 9 | ```shell 10 | git clone https://github.com/luw2007/bloomfilter.git && \ 11 | cd bloomfilter && \ 12 | pip install -r requirements.txt && \ 13 | python setup.py install 14 | ``` 15 | 16 | ### 通过源代码安装项目依赖: 17 | pyreBloom, hiredis(非必须) 18 | ```shell 19 | git clone https://github.com/redis/hiredis.git /root/src/hiredis && \ 20 | cd /root/src/hiredis && \ 21 | make && make PREFIX=/usr install &&\ 22 | ldconfig 23 | git clone https://github.com/seomoz/pyreBloom /root/src/pyreBloom && \ 24 | cd /root/src/pyreBloom && \ 25 | python setup.py install 26 | ``` 27 | 28 | ## 使用方法: 29 | ```python 30 | >>> from bloomfilter.base import BaseModel 31 | >>> class TestModel(BaseModel): 32 | ... PREFIX = "bf:test" 33 | >>> t = TestModel() 34 | >>> t.add('hello') 35 | 1 36 | >>> t.extend(['hi', 'world']) 37 | 2 38 | >>> t.contains('hi') 39 | True 40 | >>> t.delete() 41 | ``` 42 | ## 可用方法: 43 | extend, keys, contains, add, put, hashes, bits, delete 44 | 45 | ## 例子 46 | 请查看[example](examples)目录 47 | 48 | 49 | ## Q&A 50 | 1. 运行报错, 缺少pyreBloom? 51 | ``` 52 | In [1]: from bloomfilter.base import BaseModel 53 | --------------------------------------------------------------------------- 54 | ModuleNotFoundError Traceback (most recent call last) 55 | in 56 | ----> 1 from bloomfilter.base import BaseModel 57 | 58 | ~/github/bloomfilter/bloomfilter/base.py in 59 | 52 import logging 60 | 53 from six import PY3 as IS_PY3 61 | ---> 54 from pyreBloom import pyreBloom, pyreBloomException 62 | 55 63 | 56 from bloomfilter.utils import force_utf8 64 | 65 | ModuleNotFoundError: No module named 'pyreBloom' 66 | ``` 67 | 安装`pyreBloom`,pip install -r requirements.txt 68 | -------------------------------------------------------------------------------- /bloomfilter/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | bloomfilter 过滤器 4 | """ 5 | -------------------------------------------------------------------------------- /bloomfilter/base.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | bloom filter 基础模块 4 | 5 | 可用方法: 6 | extend, keys, contains, add, put, hashes, bits, delete 7 | 8 | 使用方法: 9 | >>> class TestModel(BaseModel): 10 | ... PREFIX = "bf:test" 11 | >>> t = TestModel() 12 | >>> t.add('hello') 13 | 1 14 | >>> t.extend(['hi', 'world']) 15 | 2 16 | >>> t.contains('hi') 17 | True 18 | >>> t.delete() 19 | 20 | 原生pyreBloom方法: 21 | 22 | cdef class pyreBloom(object): 23 | 24 | cdef bloom.pyrebloomctxt context 25 | cdef bytes 26 | 27 | property bits: 28 | // redis中占用大小 29 | 30 | property hashes: 31 | // 使用的hash方法数 32 | 33 | def delete(self): 34 | // 删除,会在redis中删除 35 | 36 | def put(self, value): 37 | // 添加 底层方法, 不建议直接调用 38 | 39 | def add(self, value): 40 | // 添加单个元素,调用put方法 41 | 42 | def extend(self, values): 43 | // 添加一组元素,调用put方法 44 | 45 | def contains(self, value): 46 | // 检查是否存在,当`value`可以迭代时,返回`[value]`, 否则返回`bool` 47 | 48 | def keys(self): 49 | // 在redis中存储的key列表 50 | 51 | ''' 52 | import logging 53 | from six import PY3 as IS_PY3 54 | from pyreBloom import pyreBloom, pyreBloomException 55 | 56 | from bloomfilter.utils import force_utf8 57 | 58 | 59 | class BaseModel(object): 60 | ''' 61 | bloom filter 基础模块 62 | 参数: 63 | SLOT: 可用方法类型 64 | PREFIX: redis前缀 65 | BF_SIZE: 存储最大值 66 | BF_ERROR: 允许的出错率 67 | RETRIES: 连接重试次数 68 | host: redis 服务器IP 69 | port: redis 服务器端口 70 | db: redis 服务器DB 71 | _bf_conn: 内部保存`pyreBloom`实例 72 | ''' 73 | SLOT = { 74 | 'add', 'contains', 'extend', 'keys', 'put', 'delete', 'bits', 'hashes' 75 | } 76 | PREFIX = "" 77 | BF_SIZE = 100000 78 | BF_ERROR = 0.01 79 | RETRIES = 2 80 | 81 | def __init__(self, redis=None): 82 | ''' 83 | 初始化redis配置 84 | :param redis: redis 配置 85 | ''' 86 | # 这里初始化防止类静态变量多个继承类复用,导致数据被污染 87 | self._bf_conn = None 88 | 89 | self._conf = { 90 | 'host': '127.0.0.1', 91 | 'password': '', 92 | 'port': 6379, 93 | 'db': 0 94 | } 95 | 96 | if redis: 97 | for k, v in redis.items(): 98 | if k in self._conf: 99 | self._conf[k] = redis[k] 100 | self._conf = force_utf8(self._conf) 101 | 102 | @property 103 | def bf_conn(self): 104 | ''' 105 | 初始化pyreBloom 106 | ''' 107 | if not self._bf_conn: 108 | prefix = force_utf8(self.PREFIX) 109 | logging.debug( 110 | 'pyreBloom connect: redis://%s:%s/%s, (%s %s %s)', 111 | self._conf['host'], 112 | self._conf['port'], 113 | self._conf['db'], 114 | prefix, 115 | self.BF_SIZE, 116 | self.BF_ERROR, 117 | ) 118 | self._bf_conn = pyreBloom(prefix, self.BF_SIZE, self.BF_ERROR, 119 | **self._conf) 120 | return self._bf_conn 121 | 122 | def __getattr__(self, method): 123 | '''调用pyrebloom方法 124 | 没有枚举的方法将从`pyreBloom`中获取 125 | :param method: 126 | :return: pyreBloom.{method} 127 | ''' 128 | # 只提供内部方法 129 | if method not in self.SLOT: 130 | raise NotImplementedError() 131 | 132 | # 捕获`pyreBloom`的异常, 打印必要的日志 133 | def catch_error(*a, **kwargs): 134 | '''多次重试服务''' 135 | args = force_utf8(a) 136 | kwargs = force_utf8(kwargs) 137 | for _ in range(self.RETRIES): 138 | try: 139 | func = getattr(self.bf_conn, method) 140 | res = func(*args, **kwargs) 141 | # python3 返回值和python2返回值不相同, 142 | # 手工处理返回类型 143 | if method == 'contains' and IS_PY3: 144 | if isinstance(res, list): 145 | return [i.decode('utf8') for i in res] 146 | return res 147 | except pyreBloomException as error: 148 | logging.warn('pyreBloom Error: %s %s', method, str(error)) 149 | self.reconnect() 150 | if _ == self.RETRIES: 151 | logging.error('pyreBloom Error') 152 | raise error 153 | 154 | return catch_error 155 | 156 | def __contains__(self, item): 157 | '''跳转__contains__方法 158 | :param item: 查询元素列表/单个元素 159 | :type item: list/basestring 160 | :return: [bool...]/bool 161 | ''' 162 | return self.contains(item) 163 | 164 | def reconnect(self): 165 | ''' 166 | 重新连接bloom 167 | `pyreBloom` 连接使用c driver,没有提供timeout参数,使用了内置的timeout 168 | 同时为了保证服务的可靠性,增加了多次重试机制。 169 | ``` 170 | struct timeval timeout = { 1, 5000 }; 171 | ctxt->ctxt = redisConnectWithTimeout(host, port, timeout); 172 | ``` 173 | del self._bf_conn 会调用`pyreBloom`内置的C的del方法,会关闭redis连接 174 | ''' 175 | if self._bf_conn: 176 | logging.debug('pyreBloom reconnect') 177 | del self._bf_conn 178 | self._bf_conn = None 179 | _ = self.bf_conn 180 | -------------------------------------------------------------------------------- /bloomfilter/mock.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | 测试使用的基类 4 | 使用方法: 5 | 6 | from bloomfilter import mock_bloomfilter 7 | mock_bloomfilter() 8 | 9 | ''' 10 | 11 | def mock_bloomfilter(): 12 | '''monkey patch''' 13 | import bloomfilter.base 14 | bloomfilter.base.BaseModel = MockBaseModel 15 | 16 | 17 | class MockBaseModel(object): 18 | '''模拟pyreBloom 19 | 'add', 'contains', 'extend', 'keys', 'put', 'bits', 'hashes' 20 | ''' 21 | _instance = None 22 | 23 | def __new__(cls, *args, **kwargs): 24 | '''实现单例''' 25 | if not cls._instance: 26 | cls._instance = super(MockBaseModel, cls).__new__( 27 | cls, *args, **kwargs) 28 | return cls._instance 29 | 30 | def __init__(self, redis=None): 31 | '''初始化redis配置''' 32 | if not redis: 33 | redis = {'host': '127.0.0.1', 'port': 6379, 'db': 0} 34 | self.host = redis['host'] 35 | self.port = redis['port'] 36 | self.db = redis['db'] 37 | self._cache = set() 38 | 39 | def add(self, items): 40 | '''添加''' 41 | if isinstance(items, (tuple, list)): 42 | result = [item for item in items if item not in self._cache] 43 | for i in result: 44 | self._cache.add(i) 45 | return len(result) 46 | else: 47 | if items in self._cache: 48 | return 0 49 | else: 50 | self._cache.add(items) 51 | return 1 52 | 53 | def contains(self, items): 54 | '''检查是否存在''' 55 | if isinstance(items, (tuple, list)): 56 | return [item for item in items if item in self._cache] 57 | else: 58 | return items in self._cache 59 | 60 | def delete(self): 61 | '''清理''' 62 | self._cache = set() 63 | 64 | def extend(self, items): 65 | '''批量添加''' 66 | return sum(self.add(item) for item in items) 67 | 68 | def keys(self): 69 | """ Return a list of the keys used in this bloom filter """ 70 | return [] 71 | 72 | def __contains__(self, item): 73 | """ x.__contains__(y) <==> y in x """ 74 | return self.contains(item) 75 | 76 | bits = property(lambda self: object(), lambda self, v: None, 77 | lambda self: None) 78 | 79 | hashes = property(lambda self: object(), lambda self, v: None, 80 | lambda self: None) 81 | -------------------------------------------------------------------------------- /bloomfilter/test_base.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from unittest import TestCase 3 | 4 | from bloomfilter.base import BaseModel 5 | from bloomfilter.mock import MockBaseModel 6 | 7 | 8 | class CommonModel(BaseModel): 9 | '''普通model''' 10 | PREFIX = "bf:test" 11 | 12 | 13 | class CommonMockModel(MockBaseModel): 14 | '''mock model''' 15 | PREFIX = "bf:mock" 16 | 17 | 18 | class TestBaseModel(TestCase): 19 | '''测试基本方法''' 20 | 21 | def setUp(self): 22 | '''初始化''' 23 | self.p = CommonModel() 24 | 25 | def tearDown(self): 26 | '''删除''' 27 | self.p.delete() 28 | 29 | def test_common(self): 30 | '''测试''' 31 | self.assertEqual(self.p.add('one'), 1) 32 | self.assertEqual(self.p.contains('one'), True) 33 | 34 | # 重复添加 35 | self.assertEqual(self.p.add('one'), 0) 36 | 37 | # 不存在查询 38 | self.assertEqual(self.p.contains('two'), False) 39 | 40 | # 批量添加后查询 41 | self.assertEqual(self.p.extend(['one', 'three']), 1) 42 | self.assertEqual(self.p.extend(['four', 'five']), 2) 43 | self.assertEqual( 44 | self.p.contains(['one', 'two', 'three']), ['one', 'three']) 45 | 46 | # 删除后查询 47 | self.p.delete() 48 | self.assertEqual(self.p.contains('one'), False) 49 | 50 | 51 | class TestMockBaseModel(TestBaseModel): 52 | '''测试mock方法''' 53 | 54 | def setUp(self): 55 | '''初始化''' 56 | self.p = CommonMockModel() 57 | 58 | def tearDown(self): 59 | '''删除''' 60 | self.p.delete() 61 | -------------------------------------------------------------------------------- /bloomfilter/test_utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | 3 | import os 4 | import sys 5 | sys.path.append(os.path.dirname(os.path.split(os.path.realpath(__file__))[0])) 6 | 7 | import six 8 | import unittest 9 | 10 | from bloomfilter.utils import force_utf8 11 | 12 | 13 | class TestUtils(unittest.TestCase): 14 | """工具方法测试""" 15 | 16 | def setUp(self): 17 | """setUp""" 18 | 19 | def tearDown(self): 20 | """tearDown""" 21 | pass 22 | 23 | def test_force_utf8(self): 24 | """测试force_utf8""" 25 | if six.PY3: 26 | self._assert_py3() 27 | else: 28 | self._assert_py2() 29 | 30 | def _assert_py2(self): 31 | """assert py2""" 32 | val_str = u'abc' 33 | val1 = force_utf8(val_str) 34 | self.assertEqual(val1, 'abc') 35 | 36 | val_tuple = (u'a', u'b', u'c') 37 | val2 = force_utf8(val_tuple) 38 | self.assertEqual(val2, ('a', 'b', 'c')) 39 | 40 | val_list = [u'a', u'b', u'c'] 41 | val3 = force_utf8(val_list) 42 | self.assertEqual(val3, ['a', 'b', 'c']) 43 | 44 | val_dict = {'a': u'1', 'b': u'2'} 45 | val4 = force_utf8(val_dict) 46 | self.assertEqual(val4, {'a': '1', 'b': '2'}) 47 | 48 | def _assert_py3(self): 49 | """assert py3""" 50 | val_str = 'abc' 51 | val1 = force_utf8(val_str) 52 | self.assertEqual(val1, b'abc') 53 | 54 | val_tuple = ('a', 'b', 'c') 55 | val2 = force_utf8(val_tuple) 56 | self.assertEqual(val2, (b'a', b'b', b'c')) 57 | 58 | val_list = ['a', 'b', 'c'] 59 | val3 = force_utf8(val_list) 60 | self.assertEqual(val3, [b'a', b'b', b'c']) 61 | 62 | val_dict = {'a': '1', 'b': '2'} 63 | val4 = force_utf8(val_dict) 64 | self.assertEqual(val4, {'a': b'1', 'b': b'2'}) 65 | 66 | 67 | if __name__ == '__main__': 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /bloomfilter/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | # pylint: disable=redefined-builtin,invalid-name 3 | 4 | import six 5 | 6 | if six.PY3: 7 | unicode = str 8 | 9 | 10 | def force_utf8(data): 11 | """强转utf8 12 | 13 | :param data: data 14 | :return 转换后的数据 15 | """ 16 | if isinstance(data, unicode): 17 | return _cover_utf8(data) 18 | elif isinstance(data, tuple): 19 | data = tuple([force_utf8(item) for item in data]) 20 | elif isinstance(data, list): 21 | for idx, i in enumerate(data): 22 | data[idx] = force_utf8(i) 23 | elif isinstance(data, dict): 24 | for i in data: 25 | data[i] = force_utf8(data[i]) 26 | return data 27 | 28 | 29 | def _cover_utf8(value): 30 | """强转utf8 31 | 32 | :param value: value 33 | :return utf8类型的数据 34 | """ 35 | return value.encode('utf8') \ 36 | if isinstance(value, unicode) else value 37 | -------------------------------------------------------------------------------- /doc/bloomfilter_in_action.md: -------------------------------------------------------------------------------- 1 | bloomfilter过滤器实战 2 | === 3 | 4 | 为什么引入 5 | --- 6 | 我们的业务中经常会遇到穿库的问题,通常可以通过缓存解决。 7 | 如果数据维度比较多,结果数据集合比较大时,缓存的效果就不明显了。 8 | 因此为了解决穿库的问题,我们引入Bloom Filter。 9 | 10 | 我们先看看`一般业务缓存流程`: 11 | ![缓存流程图](images/bloomfilter-cache-mysql.png) 12 | 13 | 先查询缓存,缓存不命中再查询数据库。 14 | 然后将查询结果放在缓存中即使数据不存在,也需要创建一个缓存,用来防止穿库。这里需要区分一下数据是否存在。 15 | 如果数据不存在,缓存时间可以设置相对较短,防止因为主从同步等问题,导致问题被放大。 16 | 17 | 这个流程中存在薄弱的问题是,当用户量太大时,我们会缓存大量数据空数据,并且一旦来一波冷用户,会造成雪崩效应。 18 | 对于这种情况,我们产生第二个版本流程:`redis过滤冷用户缓存流程` 19 | ![redis过滤冷用户的缓存流程图](images/bloomfilter-redis-cache-mysql.png) 20 | 21 | 我们将数据库里面中命中的用户放在redis的set类型中,设置不过期。 22 | 这样相当把redis当作数据库的索引,只要查询redis,就可以知道是否数据存在。 23 | redis中不存在就可以直接返回结果。 24 | 如果存在就按照上面提到`一般业务缓存流程`处理。 25 | 26 | 聪明的你肯定会想到更多的问题: 27 | 28 | 1. redis本身可以做缓存,为什么不直接返回数据呢? 29 | 2. 如果数据量比较大,单个set,会有性能问题? 30 | 3. 业务不重要,将全量数据放在redis中,占用服务器大量内存。投入产出不成比例? 31 | 32 | 问题1需要区分业务场景,结果数据少,我们是可以直接使用redis作为缓存,直接返回数据。 33 | 结果比较大就不太适合用redis存放了。比如ugc内容,一个评论里面可能存在上万字,业务字段多。 34 | 35 | redis使用有很多技巧。bigkey 危害比较大,无论是扩容或缩容带来的内存申请释放, 36 | 还是查询命令使用不当导致大量数据返回,都会影响redis的稳定。这里就不细谈原因及危害了。 37 | 解决bigkey 方法很简单。我们可以使用hash函数来分桶,将数据分散到多个key中。 38 | 减少单个key的大小,同时不影响查询效率。 39 | 40 | 问题3是redis存储占用内存太大。因此我们需要减少内存使用。 41 | 重新思考一下引入redis的目的。 42 | redis像一个集合,整个业务就是验证请求的参数是否在集合中。 43 | ![过滤器](images/bloomfilter-filter.png) 44 | 这个结构就像洗澡的时候用的双向阀门:左边热水,右边冷水。 45 | 46 | 大部分的编程语言都内置了filter。 47 | 拿`python`举例,filter函数用于过滤序列, 48 | 过滤掉不符合条件的元素,返回由符合条件元素组成的列表。 49 | 50 | 我们看个例子: 51 | 52 | $ python2 53 | Python 2.7.10 (default, Oct 6 2017, 22:29:07) 54 | [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.31)] on darwin 55 | Type "help", "copyright", "credits" or "license" for more information. 56 | >>> s = {2, 4} 57 | >>> filter(lambda x:x in s, [0, 1, 2]) 58 | [2] 59 | 60 | 集合s中存在 2,4两个数字,我们需要查询 0,1,2 那些在集合s中。 61 | `lambda x:x in s`构造一个匿名函数,判断入参x是否在集合s中。 62 | 过滤器filter依次对列表中的数字执行匿名函数。最终返回列表`[2]`。 63 | 64 | redis中实现set用了两种结构:intset和hash table。 65 | 非数字或者大量数字时都会退化成hash table。 66 | 那么是否好的算法可以节省hash table的大小呢? 67 | 68 | 其实早在1970年由`Burton Howard Bloom`提出的布隆过滤器(英语:Bloom Filter)。 69 | 它实际上是一个很长的二进制向量和一系列随机映射函数。 70 | 布隆过滤器可以用于检索一个元素是否在一个集合中。 71 | 它的优点是空间效率和查询时间都远远超过一般的算法, 72 | 缺点是有一定的误识别率和删除困难。 73 | 74 | BloomFilter原理 75 | --- 76 | 我们常见的将业务字段拼接之后md5,放在一个集合中。 77 | md5生成一个固定长度的128bit的串。 78 | 如果我们用bitmap来表示,则需要 79 | ``` 80 | 2**128 = 340282366920938463463374607431768211456 bit 81 | ``` 82 | 判断一个值在不在,就变成在这个bitmap中判断所在位是否为1。 83 | 但是我们全世界的机器存储空间也无法存储下载。 84 | 因此我们只能分配有限的空间来存储。 85 | 比如: 86 | ```python 87 | import crc32 88 | 89 | def BloomFilter(sample, size, hash_size=1): 90 | # 构造一个hash函数,将输入数据散列到size一个位置上 91 | hash = lambda x:crc32(str(x).encode())%size 92 | collision, s = 0, set() 93 | for i in range(sample): 94 | k = set() 95 | for j in range(hash_size): 96 | k.add(hash(i+j*size/hash_size)) 97 | # 只有所有散列结果k都在s中,才认为i重复 98 | if not k - s: 99 | collision += 1 100 | continue 101 | # 将散列结果k更新到集合s中 102 | s |= k 103 | return collision 104 | ``` 105 | 当只有一个hash函数时:很容易发生冲突。 106 | ![one_hash](images/bloomfilter-one-hash.png) 107 | 116 | 可以看到上面1和2的hash结果都是7,发生冲突。 117 | 如果增加hash函数,会发生什么情况? 118 | 119 | ![more_hash](images/bloomfilter-more-hash.png) 120 | 129 | 130 | 我们使用更多的hash函数和更大的数据集合来测试。得到下面这张表 131 | ![碰撞表](images/bloomfilter-collision.png) 132 | 156 | 157 | 由此可以看到当增加hash方法能够有效的降低碰撞机率。 158 | 比较好的数据如下: 159 | ![碰撞表100](images/bloomfilter-collision-100.png) 160 | 168 | 169 | 但是增加了hash方法之后,会降低空间的使用效率。当集合占用总体空间达到25%的时候, 170 | 增加hash 的效果已经不明显 171 | 172 | ![碰撞表250](images/bloomfilter-collision-250.png) 173 | 181 | 182 | 上面的使用多个hash方法来降低碰撞就是BloomFilter的核心思想。 183 | 184 | 185 | 适合的场景 186 | --- 187 | - 数据库防止穿库 188 | Google Bigtable,Apache HBase和Apache Cassandra以及Postgresql 使用BloomFilter来减少不存在的行或列的磁盘查找。避免代价高昂的磁盘查找会大大提高数据库查询操作的性能。 189 | 如同一开始的业务场景。如果数据量较大,不方便放在缓存中。需要对请求做拦截防止穿库。 190 | 191 | - 缓存宕机 192 | 缓存宕机的场景,使用布隆过滤器会造成一定程度的误判。原因是除了Bloom Filter 本身有误判率,宕机之前的缓存不一定能覆盖到所有DB中的数据,当宕机后用户请求了一个以前从未请求的数据,这个时候就会产生误判。当然,缓存宕机时使用布隆过滤器作为应急的方式,这种情况应该也是可以忍受的。 193 | 194 | - WEB拦截器 195 | 相同请求拦截防止被攻击。用户第一次请求,将请求参数放入BloomFilter中,当第二次请求时,先判断请求参数是否被BloomFilter命中。可以提高缓存命中率 196 | 197 | - 恶意地址检测 198 | chrome 浏览器检查是否是恶意地址。 199 | 首先针对本地BloomFilter检查任何URL,并且仅当BloomFilter返回肯定结果时才对所执行的URL进行全面检查(并且用户警告,如果它也返回肯定结果)。 200 | 201 | - 比特币加速 202 | bitcoin 使用BloomFilter来加速钱包同步。 203 | 204 | 算法优点: 205 | --- 206 | - 数据空间小,不用存储数据本身。 207 | 208 | 算法本身缺点: 209 | --- 210 | - 元素可以添加到集合中,但不能被删除。 211 | - 匹配结果只能是“绝对不在集合中”,并不能保证匹配成功的值已经在集合中。 212 | - 当集合快满时,即接近预估最大容量时,误报的概率会变大。 213 | - 数据占用空间放大。一般来说,对于1%的误报概率,每个元素少于10比特,与集合中的元素的大小或数量无关。 214 | - 查询过程变慢,hash函数增多,导致每次匹配过程,需要查找多个位(hash个数)来确认是否存在。 215 | 216 | 对于BloomFilter的优点来说,缺点都可以忽略。毕竟只需要kN的存储空间就能存储N个元素。空间效率十分优秀。 217 | 218 | 如何使用BloomFilter 219 | --- 220 | BloomFilter 需要一个大的bitmap来存储。鉴于目前公司现状,最好的存储容器是redis。 221 | 从[github topics: bloom-filter](https://github.com/topics/bloom-filter)中经过简单的调研。 222 | 223 | redis集成BloomFilter方案: 224 | - 原生python 调用setbit 构造 BloomFilter 225 | - [lua脚本](https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter) 226 | - [Rebloom - Bloom Filter Module for Redis](https://github.com/RedisLabsModules/rebloom) (注:redis Module在redis4.0引入) 227 | - 使用hiredis 调用redis [pyreBloom](https://github.com/seomoz/pyreBloom) 228 | 229 | 原生python 方法太慢,lua脚本和module 部署比较麻烦。于是我们推荐使用pyreBloom,底层使用。 230 | ```shell 231 | pyreBloom:master λ ls 232 | Makefile bloom.h bloom.pxd murmur.c pyreBloom.pyx 233 | bloom.c bloom.o main.c pyreBloom.c 234 | ``` 235 | 从文件命名上可以看到bloom 使用c编写。pyreBloom 使用cython编写。 236 | 237 | bloom.h 里面实现BloomFilter的核心逻辑,完成与redis server的交互;hash函数;添加,检查和删除方法的实现。 238 | ```c 239 | int init_pyrebloom(pyrebloomctxt * ctxt, char * key, uint32_t capacity, double error, char* host, uint32_t port, char* password, uint32_t db); 240 | int free_pyrebloom(pyrebloomctxt * ctxt); 241 | 242 | int add(pyrebloomctxt * ctxt, const char * data, uint32_t len); 243 | int add_complete(pyrebloomctxt * ctxt, uint32_t count); 244 | 245 | int check(pyrebloomctxt * ctxt, const char * data, uint32_t len); 246 | int check_next(pyrebloomctxt * ctxt); 247 | 248 | int delete(pyrebloomctxt * ctxt); 249 | ``` 250 | pyreBloom.pyx 251 | ```python 252 | import math 253 | import random 254 | 255 | cimport bloom 256 | 257 | 258 | class pyreBloomException(Exception): 259 | '''Some sort of exception has happened internally''' 260 | pass 261 | 262 | 263 | cdef class pyreBloom(object): 264 | cdef bloom.pyrebloomctxt context 265 | cdef bytes key 266 | 267 | property bits: 268 | def __get__(self): 269 | return self.context.bits 270 | 271 | property hashes: 272 | def __get__(self): 273 | return self.context.hashes 274 | 275 | def __cinit__(self, key, capacity, error, host='127.0.0.1', port=6379, 276 | password='', db=0): 277 | self.key = key 278 | if bloom.init_pyrebloom(&self.context, self.key, capacity, 279 | error, host, port, password, db): 280 | raise pyreBloomException(self.context.ctxt.errstr) 281 | 282 | def __dealloc__(self): 283 | bloom.free_pyrebloom(&self.context) 284 | 285 | def delete(self): 286 | bloom.delete(&self.context) 287 | 288 | def put(self, value): 289 | if getattr(value, '__iter__', False): 290 | r = [bloom.add(&self.context, v, len(v)) for v in value] 291 | r = bloom.add_complete(&self.context, len(value)) 292 | else: 293 | bloom.add(&self.context, value, len(value)) 294 | r = bloom.add_complete(&self.context, 1) 295 | if r < 0: 296 | raise pyreBloomException(self.context.ctxt.errstr) 297 | return r 298 | 299 | def add(self, value): 300 | return self.put(value) 301 | 302 | def extend(self, values): 303 | return self.put(values) 304 | 305 | def contains(self, value): 306 | # If the object is 'iterable'... 307 | if getattr(value, '__iter__', False): 308 | r = [bloom.check(&self.context, v, len(v)) for v in value] 309 | r = [bloom.check_next(&self.context) for i in range(len(value))] 310 | if (min(r) < 0): 311 | raise pyreBloomException(self.context.ctxt.errstr) 312 | return [v for v, included in zip(value, r) if included] 313 | else: 314 | bloom.check(&self.context, value, len(value)) 315 | r = bloom.check_next(&self.context) 316 | if (r < 0): 317 | raise pyreBloomException(self.context.ctxt.errstr) 318 | return bool(r) 319 | 320 | def __contains__(self, value): 321 | return self.contains(value) 322 | 323 | def keys(self): 324 | '''Return a list of the keys used in this bloom filter''' 325 | return [self.context.keys[i] for i in range(self.context.num_keys)] 326 | ``` 327 | 328 | ```c++ 329 | 原生pyreBloom方法: 330 | 331 | cdef class pyreBloom(object): 332 | 333 | cdef bloom.pyrebloomctxt context 334 | cdef bytes 335 | 336 | property bits: 337 | 338 | property hashes: 339 | // 使用的hash方法数 340 | 341 | def delete(self): 342 | // 删除,会在redis中删除 343 | 344 | def put(self, value): 345 | // 添加 底层方法, 不建议直接调用 346 | 347 | def add(self, value): 348 | // 添加单个元素,调用put方法 349 | 350 | def extend(self, values): 351 | // 添加一组元素,调用put方法 352 | 353 | def contains(self, value): 354 | // 检查是否存在,当`value`可以迭代时,返回`[value]`, 否则返回`bool` 355 | 356 | def keys(self): 357 | // 在redis中存储的key列表 358 | ``` 359 | 由于pyreBloom使用hiredis库,本身没有重连等逻辑,于是错了简单的封装。 360 | ```python 361 | 362 | # coding=utf-8 363 | ''' 364 | bloom filter 基础模块 365 | 366 | 可用方法: 367 | extend, keys, contains, add, put, hashes, bits, delete 368 | 369 | 使用方法: 370 | >>> class TestModel(BaseModel): 371 | ... PREFIX = "bf:test" 372 | >>> t = TestModel() 373 | >>> t.add('hello') 374 | 1 375 | >>> t.extend(['hi', 'world']) 376 | 2 377 | >>> t.contains('hi') 378 | True 379 | >>> t.delete() 380 | ''' 381 | import logging 382 | from six import PY3 as IS_PY3 383 | from pyreBloom import pyreBloom, pyreBloomException 384 | 385 | from BloomFilter.utils import force_utf8 386 | 387 | 388 | class BaseModel(object): 389 | ''' 390 | bloom filter 基础模块 391 | 参数: 392 | SLOT: 可用方法类型 393 | PREFIX: redis前缀 394 | BF_SIZE: 存储最大值 395 | BF_ERROR: 允许的出错率 396 | RETRIES: 连接重试次数 397 | host: redis 服务器IP 398 | port: redis 服务器端口 399 | db: redis 服务器DB 400 | _bf_conn: 内部保存`pyreBloom`实例 401 | ''' 402 | SLOT = {'add', 'contains', 'extend', 'keys', 'put', 'delete', 403 | 'bits', 'hashes'} 404 | PREFIX = "" 405 | BF_SIZE = 100000 406 | BF_ERROR = 0.01 407 | RETRIES = 2 408 | 409 | def __init__(self, redis=None): 410 | ''' 411 | 初始化redis配置 412 | :param redis: redis 配置 413 | ''' 414 | # 这里初始化防止类静态变量多个继承类复用,导致数据被污染 415 | self._bf_conn = None 416 | 417 | self._conf = { 418 | 'host': '127.0.0.1', 'password': '', 419 | 'port': 6379, 'db': 0 420 | } 421 | 422 | if redis: 423 | for k, v in redis.items(): 424 | if k in self._conf: 425 | self._conf[k] = redis[k] 426 | self._conf = force_utf8(self._conf) 427 | 428 | @property 429 | def bf_conn(self): 430 | ''' 431 | 初始化pyreBloom 432 | ''' 433 | if not self._bf_conn: 434 | prefix = force_utf8(self.PREFIX) 435 | logging.debug( 436 | 'pyreBloom connect: redis://%s:%s/%s, (%s %s %s)', 437 | self._conf['host'], self._conf['port'], self._conf['db'], 438 | prefix, self.BF_SIZE, self.BF_ERROR, 439 | ) 440 | self._bf_conn = pyreBloom( 441 | prefix, self.BF_SIZE, self.BF_ERROR, **self._conf) 442 | return self._bf_conn 443 | 444 | def __getattr__(self, method): 445 | '''调用pyrebloom方法 446 | 没有枚举的方法将从`pyreBloom`中获取 447 | :param method: 448 | :return: pyreBloom.{method} 449 | ''' 450 | # 只提供内部方法 451 | if method not in self.SLOT: 452 | raise NotImplementedError() 453 | 454 | # 捕获`pyreBloom`的异常, 打印必要的日志 455 | def catch_error(*a, **kwargs): 456 | '''多次重试服务''' 457 | args = force_utf8(a) 458 | kwargs = force_utf8(kwargs) 459 | for _ in range(self.RETRIES): 460 | try: 461 | func = getattr(self.bf_conn, method) 462 | res = func(*args, **kwargs) 463 | # python3 返回值和python2返回值不相同, 464 | # 手工处理返回类型 465 | if method == 'contains' and IS_PY3: 466 | if isinstance(res, list): 467 | return [i.decode('utf8') for i in res] 468 | return res 469 | except pyreBloomException as error: 470 | logging.warn( 471 | 'pyreBloom Error: %s %s', method, str(error)) 472 | self.reconnect() 473 | if _ == self.RETRIES: 474 | logging.error('pyreBloom Error') 475 | raise error 476 | 477 | return catch_error 478 | 479 | def __contains__(self, item): 480 | '''跳转__contains__方法 481 | :param item: 查询元素列表/单个元素 482 | :type item: list/basestring 483 | :return: [bool...]/bool 484 | ''' 485 | return self.contains(item) 486 | 487 | def reconnect(self): 488 | ''' 489 | 重新连接bloom 490 | `pyreBloom` 连接使用c driver,没有提供timeout参数,使用了内置的timeout 491 | 同时为了保证服务的可靠性,增加了多次重试机制。 492 | struct timeval timeout = { 1, 5000 }; 493 | ctxt->ctxt = redisConnectWithTimeout(host, port, timeout); 494 | del self._bf_conn 会调用`pyreBloom`内置的C的del方法,会关闭redis连接 495 | ''' 496 | if self._bf_conn: 497 | logging.debug('pyreBloom reconnect') 498 | del self._bf_conn 499 | self._bf_conn = None 500 | _ = self.bf_conn 501 | ``` 502 | 503 | 进阶:计数过滤器(Counting Filter) 504 | --- 505 | 提供了一种在BloomFilter上实现删除操作的方法,而无需重新重新创建过滤器。在计数滤波器中,阵列位置(桶)从单个位扩展为n位计数器。实际上,常规布隆过滤器可以被视为计数过滤器,其桶大小为一位。 506 | 507 | 插入操作被扩展为递增桶的值,并且查找操作检查每个所需的桶是否为非零。然后,删除操作包括递减每个桶的值。 508 | 509 | 存储桶的算术溢出是一个问题,并且存储桶应该足够大以使这种情况很少见。如果确实发生,则增量和减量操作必须将存储区设置为最大可能值,以便保留BloomFilter的属性。 510 | 511 | 计数器的大小通常为3或4位。因此,计算布隆过滤器的空间比静态布隆过滤器多3到4倍。相比之下, Pagh,Pagh和Rao(2005)以及Fan等人的数据结构。(2014)也允许删除但使用比静态BloomFilter更少的空间。 512 | 513 | 计数过滤器的另一个问题是可扩展性有限。由于无法扩展计数布隆过滤器表,因此必须事先知道要同时存储在过滤器中的最大键数。一旦超过表的设计容量,随着插入更多密钥,误报率将迅速增长。 514 | 515 | Bonomi等人。(2006)引入了一种基于d-left散列的数据结构,它在功能上是等效的,但使用的空间大约是计算BloomFilter的一半。此数据结构中不会出现可伸缩性问题。一旦超出设计容量,就可以将密钥重新插入到双倍大小的新哈希表中。 516 | 517 | Putze,Sanders和Singler(2007)的节省空间的变体也可用于通过支持插入和删除来实现计数过滤器。 518 | 519 | Rottenstreich,Kanizo和Keslassy(2012)引入了一种基于变量增量的新通用方法,该方法显着提高了计算布隆过滤器及其变体的误报概率,同时仍支持删除。与计数布隆过滤器不同,在每个元素插入时,散列计数器以散列变量增量而不是单位增量递增。要查询元素,需要考虑计数器的确切值,而不仅仅是它们的正面性。如果由计数器值表示的总和不能由查询元素的相应变量增量组成,则可以将否定答案返回给查询。 520 | 521 | 外链: 522 | --- 523 | - [Bloom filter](https://en.wikipedia.org/wiki/Bloom_filter) 524 | - [cuckoo filter paper](http://www.cs.cmu.edu/~binfan/papers/conext14_cuckoofilter.pdf) 525 | -------------------------------------------------------------------------------- /doc/bloomfilter_principle.md: -------------------------------------------------------------------------------- 1 | BloomFilter的原理 2 | === 3 | BloomFilter的算法描述 4 | 空bloom filter是m位的位数组,全部设置为0。还必须定义k个不同的散列函数, 5 | 每个散列函数将一些集合元素映射或散列到m个阵列位置之一,从而生成均匀的随机分布。 6 | 通常, k是常数,远小于m,其与要添加的元素的数量成比例; k的精确选择和m的比例常数由过滤器的预期误报率决定。 7 | 8 | 要添加元素,请将其提供给每个k哈希函数以获取k个数组位置。将所有这些位置的位设置为1。 9 | 10 | 要查询元素(匹配它是否在集合中,请将其提供给每个k哈希函数以获取k个数组位置。 11 | 如果这些位置的任何位为0,则该元素肯定不在该集合中 - 如果是,则插入时所有位都将设置为1。 12 | 如果全部都是1,则元素在集合中,或者在插入其他元素期间偶然将位设置为1,从而导致误报。 13 | 14 | 虽然存在误报问题,但BloomFilter比其他数据结构具有强大的空间优势。 15 | 大部分数据结构(如:平衡二叉树,前缀树,哈希表)都要求至少存储数据项本身,或者数据的一部分。 16 | 但是,BloomFilter根本不存储数据项。相比之下,具有1%误差和最佳值k的布隆过滤器每个元素仅需要大约9.6比特, 17 | 而不管元素的大小。这种优势部分来自于其紧凑性,继承自阵列,部分来自其概率性质。 18 | 通过每个元素仅添加约4.8位,可以将1%的误报率降低10倍。 19 | 20 | 误报的可能性 21 | 假设散列函数以相等的概率选择每个阵列位置。 22 | 23 | 如果m是数组中的位数,则在插入元素期间某个散列函数未将某个位设置为1的概率是 24 | ![$1-{\frac {1}{m}}$](https://latex.codecogs.com/gif.latex?1-{\frac{1}{m}}) 25 | 如果k是散列函数的数量并且彼此之间没有显着的相关性,那么任何散列函数未将该位设置为1的概率是 ![$\left(1-{\frac {1}{m}}\right)^{k}$](https://latex.codecogs.com/gif.latex?\left(1-{\frac{1}{m}}\right)^{k}) 26 | 27 | 如果我们插入了n个元素,那么某个位仍为0的概率就是 28 | ![${\displaystyle \left(1-{\frac {1}{m}}\right)^{kn}}$](https://latex.codecogs.com/gif.latex?(1-{\frac{1}{m}})^{kn}) 29 | 30 | 因此,它是1的概率 31 | ![${\displaystyle 1-\left(1-{\frac {1}{m}}\right)^{kn}}$](https://latex.codecogs.com/gif.latex?1-(1-{\frac{1}{m}})^{kn}) 32 | 现在测试不在集合中的元素的成员资格。由散列函数计算的每个k个阵列位置是1,概率如上。 33 | 所有这些都是1的概率,这将导致算法错误地声称该元素在集合中,通常作为 34 | ![${\displaystyle \left(1- \left[1-{\frac {1}{m}} \right]^{kn} \right)^{k}\approx \left(1-e^{-kn/m} \right)^{k}}$](https://latex.codecogs.com/gif.latex?E[q]=\left(1-\left[1-{\frac{1}{m}}\right]^{kn}\right)^{k}\approx\left(1-e^{-kn/m}\right)^{k}) 35 | 这不是严格正确的,因为它假定每个位被设置的概率是独立的。 36 | 37 | 然而,假设它是近似的,我们假设误差的概率随着m (阵列中的位数)的增加而减小, 38 | 并且随着n (插入的元素的数量)的增加而增加。 39 | 40 | Mitzenmacher和Upfal给出了另一种分析方法,该方法在不假设独立性的情况下达到了相同的近似值。 41 | 将所有n个项添加到布隆过滤器后,令q为设置为0的m位的一部分,即,仍设置为0的位数为qm。 42 | 然后,在测试时不在集合中的元素的成员资格,对于由任何k个散列函数给出的数组位置,该位被设置为1的概率是1-q中。 43 | 因此,所有k个散列函数将其位设置为1的概率是 ![$(1-q)^k$](https://latex.codecogs.com/gif.latex?(1-q)^k)。 44 | 此外,q的期望值是对于n个项中的每一个,由k个散列函数中的每一个保持给定阵列位置不被触及的概率,这是(如上所述) 45 | 46 | ![${\displaystyle E[q] = \left(1-{\frac {1}{m}} \right)^{kn}}$](https://latex.codecogs.com/gif.latex?E[q]=(1-{\frac{1}{m}})^{kn}) 47 | 48 | 在没有独立性假设的情况下,可以证明q非常强烈地集中在其预期值附近。 49 | 特别是,从Azuma-Hoeffding不等式 ,他们证明了 ![${\displaystyle \Pr(\left|q-E[q]\right|\geq {\frac {\lambda }{m}})\leq 2\exp(-2\lambda ^{2}/kn)}$](https://latex.codecogs.com/gif.latex?\Pr(\left|q-E[q]\right|\geq{\frac{\lambda}{m}})\leq2\exp(-2\lambda^{2}/kn)) 50 | 51 | 因此,我们可以说误报的确切概率是 52 | ![${\displaystyle \sum _{t}\Pr(q=t)(1-t)^{k}\approx (1-E[q])^{k} = \left(1- \left[1-{\frac {1}{m}} \right]^{kn} \right)^{k}\approx \left(1-e^{-kn/m} \right)^{k}}$](https://latex.codecogs.com/gif.latex?\sum_{t}\Pr(q=t)(1-t)^{k}\approx(1-E[q])^{k}=\left(1-\left[1-{\frac{1}{m}}\right]^{kn}\right)^{k}\approx\left(1-e^{-kn/m}\right)^{k}) 53 | 像之前一样。 54 | 55 | 布隆过滤器是一种紧凑表示一组项目的方法。通常尝试计算两组之间的交集或并集的大小。 56 | 布隆过滤器可用于近似交集的大小和两组的并集。Swamidass&Baldi(2007)表明,对于长度为m的两个Bloom滤波器,它们的计数分别可以估算为 57 | 58 | ![${\displaystyle n(A^{*})=-{\frac {m}{k}}\ln \left[1-{\frac {n(A)}{m}}\right]}$](https://latex.codecogs.com/gif.latex?n(A^{*})=-{\frac{m}{k}}\ln\left[1-{\frac{n(A)}{m}}\right]) 59 | 60 | 和 61 | 62 | ![${\displaystyle n(B^{*})=-{\frac {m}{k}}\ln \left[1-{\frac {n(B)}{m}}\right]}$](https://latex.codecogs.com/gif.latex?n(B^{*})=-{\frac{m}{k}}\ln\left[1-{\frac{n(B)}{m}}\right]) 63 | 64 | 他们的联合的大小可以估计为: 65 | 66 | ![${\displaystyle n(A^{ *} \cup B^{ *})=-{\frac {m}{k}}\ln \left[1-{\frac {n(A \cup B)}{m}}\right]}$](https://latex.codecogs.com/gif.latex?n(A^{*}\cup%20B^{*})=-{\frac{m}{k}}\ln[1-{\frac{n(A\cup%20B)}{m}}]) 67 | 68 | 那里![A\cupB](https://latex.codecogs.com/gif.latex?n(A\cupB))是两个BloomFilter中任何一个中设置为1的位数。 69 | 70 | 最后,交叉点可以估算为 71 | 72 | ![${\displaystyle n(A^{ *}\cap B^{ *})= n(A^{ *})+ n(B^{ *})- n(A^{ *}\cup B^{ *})}$](https://latex.codecogs.com/gif.latex?n(A^*%20\cap%20B^{*})=n(A^{*})+n(B^{*})-n(A^{*}%20\cup%20B^{*})) 73 | 74 | 一起使用这三个公式。 75 | 76 | 了解原理之后,我们在使用的过程中还要确定空间大小和使用的hash数量。 77 | 我们可以通过`bloom filter calculator`来计算。 78 | 79 | - https://hur.st/bloomfilter/ 80 | - https://www.di-mgt.com.au/bloom-calculator.html 81 | - http://www.ccs.neu.edu/home/pete/bloom-filters/calculator.html 82 | -------------------------------------------------------------------------------- /doc/images/bloomfilter-cache-mysql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luw2007/bloomfilter/81e8ea16aae25005b32d5c39b9b3ae246a254a46/doc/images/bloomfilter-cache-mysql.png -------------------------------------------------------------------------------- /doc/images/bloomfilter-collision-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luw2007/bloomfilter/81e8ea16aae25005b32d5c39b9b3ae246a254a46/doc/images/bloomfilter-collision-100.png -------------------------------------------------------------------------------- /doc/images/bloomfilter-collision-250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luw2007/bloomfilter/81e8ea16aae25005b32d5c39b9b3ae246a254a46/doc/images/bloomfilter-collision-250.png -------------------------------------------------------------------------------- /doc/images/bloomfilter-collision.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luw2007/bloomfilter/81e8ea16aae25005b32d5c39b9b3ae246a254a46/doc/images/bloomfilter-collision.png -------------------------------------------------------------------------------- /doc/images/bloomfilter-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luw2007/bloomfilter/81e8ea16aae25005b32d5c39b9b3ae246a254a46/doc/images/bloomfilter-filter.png -------------------------------------------------------------------------------- /doc/images/bloomfilter-more-hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luw2007/bloomfilter/81e8ea16aae25005b32d5c39b9b3ae246a254a46/doc/images/bloomfilter-more-hash.png -------------------------------------------------------------------------------- /doc/images/bloomfilter-one-hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luw2007/bloomfilter/81e8ea16aae25005b32d5c39b9b3ae246a254a46/doc/images/bloomfilter-one-hash.png -------------------------------------------------------------------------------- /doc/images/bloomfilter-redis-cache-mysql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luw2007/bloomfilter/81e8ea16aae25005b32d5c39b9b3ae246a254a46/doc/images/bloomfilter-redis-cache-mysql.png -------------------------------------------------------------------------------- /examples/read.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | # pylint: disable=invalid-name 3 | 4 | from bloomfilter.base import BaseModel 5 | from bloomfilter.conf import USER_READ 6 | 7 | REDIS_CONF = {'host': '127.0.0.1', 'port': 6379, 'db': 0} 8 | USER_READ = { 9 | 'redis': REDIS_CONF, 10 | 'key': 'bf:user_read', 11 | 'size': 1000000, 12 | } 13 | 14 | class UserReadModel(BaseModel): 15 | """用户阅读时长过滤器""" 16 | 17 | PREFIX = USER_READ['key'] 18 | BF_SIZE = USER_READ['size'] 19 | 20 | def set(self, user_name): 21 | """用户更新了总阅读时长 22 | 23 | :param user_name: 用户名 24 | """ 25 | return self.add(user_name) 26 | 27 | def is_set(self, user_name): 28 | """检查用户是否更新了总阅读时长 29 | 30 | :param user_name: 用户名 31 | """ 32 | return self.contains(user_name) 33 | 34 | 35 | user_read_filter = UserReadModel(redis=USER_READ['redis']) 36 | 37 | if __name__ == "__main__": 38 | user_read_filter.set('i') 39 | user_read_filter.is_set('i') 40 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e git+https://github.com/seomoz/pyreBloom.git#egg=pyreBloom 2 | -e git+https://github.com/redis/hiredis.git#egg=hiredis 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding:utf8 2 | ''' 安装脚本 3 | ''' 4 | 5 | from setuptools import setup, find_packages 6 | 7 | setup( 8 | name="bloomfilter", 9 | version="0.2.0", 10 | description="bloomfilter sdk", 11 | author="luw2007", 12 | author_email="luw2007@gmail.com", 13 | packages=find_packages(), 14 | ) 15 | --------------------------------------------------------------------------------