1、什么是布隆过滤器
布隆过滤器是一种概率空间高效的数据结构,特点是高效地插入和查询,用来告诉你 “某样东西一定不存在或者可能存在”。
相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
2、实现原理
(1)我们先来仔细地看看它的空间效率。如果你想在集合中存储一系列的元素,有很多种不同的做法。你可以把数据映射到 HashMap 的 Key,然后在 O(1) 的时间复杂度内,hashmap的插入和查询的效率都非常高。但是,由于hashmap直接存储内容,所以空间利用率并不高。一旦你的值很多例如上亿的时候,那 HashMap 占据的内存大小就变得很可观了。还比如说你的数据集存储在远程服务器上,本地服务接受输入,而数据集非常大不可能一次性读进内存构建 HashMap 的时候,也会存在问题。
(2)如果希望提高空间利用率,我们可以在元素插入集合之前做一次哈希变换。还有其它方法呢?我们可以用位数组来存储元素的哈希值。还有吗,还有吗?我们也允许在位数组中存在哈希冲突。这正是布隆过滤器的工作原理,它们就是基于允许哈希冲突的位数组,可能会造成一些误报。在布隆过滤器的设计阶段就允许哈希冲突的存在,否则空间使用就不够紧凑了。
3、布隆过滤器基础
布隆过滤器是N位的位数组,其中N是位数组的大小。它还有另一个参数k,表示使用哈希函数的个数。这些哈希函数用来设置位数组的值。当往过滤器中插入元素x时,h1(x), h2(x), ..., hk(x)所对应索引位置的值被置为“1”,索引值由各个哈希函数计算得到。注意,如果我们增加哈希函数的数量,误报的概率会趋近于0.但是,插入和查找的时间开销更大,布隆过滤器的容量也会减小。
为了用布隆过滤器检验元素是否存在,我们需要校验是否所有的位置都被置“1”,与我们插入元素的过程非常相似。如果所有位置都被置“1”,那也就意味着该元素很有可能存在于布隆过滤器中。若有位置未被置“1”,那该元素一定不存在。
4、布隆过滤器数据结构
布隆过滤器是一个 bit 向量或者说 bit 数组,长这样:
现在我们新建一个长度为16的布隆过滤器,默认值都是0,就像下面这样:
现在需要添加一个数据:
我们通过某种计算方式,比如Hash1,计算出了Hash1(数据)=5,我们就把下标为5的格子改成1,就像下面这样:
我们又通过某种计算方式,比如Hash2,计算出了Hash2(数据)=9,我们就把下标为9的格子改成1,就像下面这样:
还是通过某种计算方式,比如Hash3,计算出了Hash3(数据)=2,我们就把下标为2的格子改成1,就像下面这样:
这样,刚才添加的数据就占据了布隆过滤器“5”,“9”,“2”三个格子。
可以看出,仅仅从布隆过滤器本身而言,根本没有存放完整的数据,只是运用一系列随机映射函数计算出位置,然后填充二进制向量。
你只需利用上面的三种固定的计算方式,计算出这个数据占据哪些格子,然后看看这些格子里面放置的是否都是1,如果有一个格子不为1,那么就代表这个数字不在其中。这很好理解吧,比如现在又给你了刚才你添加进去的数据,你通过三种固定的计算方式,算出的结果肯定和上面的是一模一样的,也是占据了布隆过滤器“5”,“9”,“2”三个格子。
如果这些格子里面放置的都是1,不一定代表给定的数据一定重复,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 数据 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置为了 1 ,那么程序还是会判断 数据 这个值存在。
注意,如果我们增加哈希函数的数量,误报的概率会趋近于0.但是,插入和查找的时间开销更大,布隆过滤器的容量也会减小。
5、布隆过滤器的优缺点:
优点:
(1)由于存放的不是完整的数据,所以占用的内存很少,而且新增,查询速度够快;
缺点:
(1)随着数据的增加,误判率随之增加;无法做到删除数据;只能判断数据是否一定不存在,而无法判断数据是否一定存在。
(2)无法返回元素本身
(3)无法删除某个元素
(4)在不同语言中的实现,有两个原因,其中之一是选择好的哈希函数和实现方法能有效改善错误率的分布。其次,它需要通过实战测试,错误率和容量大小都要经得起实战检验。
6、布隆过滤器应用
(1) 缓存穿透
我们经常会把一部分数据放在Redis等缓存,比如产品详情。这样有查询请求进来,我们可以根据产品Id直接去缓存中取数据,而不用读取数据库,这是提升性能最简单,最普遍,也是最有效的做法。一般的查询请求流程是这样 的:先查缓存,有缓存的话直接返回,如果缓存中没有,再去数据库查询,然后再把数据库取出来的数据放入缓存,一切看起来很美好。但是如果现在有大量请求进来,而且都在请求一个不存在的产品Id,会发生什么?既然产品Id都 不存在,那么肯定没有缓存,没有缓存,那么大量的请求都怼到数据库,数据库的压力一下子就上来了,还有可能直接导致数据库挂掉。
虽然有很多办法都可以解决这问题,但是我们的主角是“布隆过滤器”,没错,“布隆过滤器”就可以解决(缓解)缓存穿透问题。至于为什么说是“缓解”,看下去你就明白了。
(2) 大量数据,判断给定的是否在其中
现在有大量的数据,而这些数据的大小已经远远超出了服务器的内存,现在再给你一个数据,如何判断给你的数据在不在其中。如果服务器的内存足够大,那么用HashMap是一个不错的解决方案,理论上的时间复杂度可以达到 O(1),但是现在数据的大小已经远远超出了服务器的内存,所以无法使用HashMap,这个时候就可以使用“布隆过滤器”来解决这个问题。但是还是同样的,会有一定的“误判率”。
(3) 爬虫url的去重
7、Python实现布隆过滤器
1、第一种
import mmh3 import redis import math import time class PyBloomFilter(): #内置100个随机种子 SEEDS = [543, 460, 171, 876, 796, 607, 650, 81, 837, 545, 591, 946, 846, 521, 913, 636, 878, 735, 414, 372, 344, 324, 223, 180, 327, 891, 798, 933, 493, 293, 836, 10, 6, 544, 924, 849, 438, 41, 862, 648, 338, 465, 562, 693, 979, 52, 763, 103, 387, 374, 349, 94, 384, 680, 574, 480, 307, 580, 71, 535, 300, 53, 481, 519, 644, 219, 686, 236, 424, 326, 244, 212, 909, 202, 951, 56, 812, 901, 926, 250, 507, 739, 371, 63, 584, 154, 7, 284, 617, 332, 472, 140, 605, 262, 355, 526, 647, 923, 199, 518] #capacity是预先估计要去重的数量 #error_rate表示错误率 #conn表示redis的连接客户端 #key表示在redis中的键的名字前缀 def __init__(self, capacity=1000000000, error_rate=0.00000001, conn=None, key='BloomFilter'): self.m = math.ceil(capacity*math.log2(math.e)*math.log2(1/error_rate)) #需要的总bit位数 self.k = math.ceil(math.log1p(2)*self.m/capacity) #需要最少的hash次数 self.mem = math.ceil(self.m/8/1024/1024) #需要的多少M内存 self.blocknum = math.ceil(self.mem/512) #需要多少个512M的内存块,value的第一个字符必须是ascii码,所有最多有256个内存块 self.seeds = self.SEEDS[0:self.k] self.key = key self.N = 2**31-1 self.redis = conn print(self.m) print(self.k) print(self.mem) print(self.k) def add(self, value): name = self.key + "_" + str(ord(value[0])%self.blocknum) hashs = self.get_hashs(value) for hash in hashs: self.redis.setbit(name, hash, 1) def is_exist(self, value): name = self.key + "_" + str(ord(value[0])%self.blocknum) hashs = self.get_hashs(value) exist = True for hash in hashs: exist = exist & self.redis.getbit(name, hash) return exist def get_hashs(self, value): hashs = list() for seed in self.seeds: hash = mmh3.hash(value, seed) if hash >= 0: hashs.append(hash) else: hashs.append(self.N - hash) return hashs pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=0) conn = redis.Redis(connection_pool=pool) start = time.time() bf = PyBloomFilter(conn=conn) bf.add('www.jobbole.com') bf.add('www.zhihu.com') print(bf.is_exist('www.zhihu.com')) print(bf.is_exist('www.lagou.com'))
第二种
(1)生成hash方法
# #************************************************************************** #* * #* General Purpose Hash Function Algorithms Library * #* * #* Author: Arash Partow - 2002 * #* URL: http://www.partow.net * #* URL: http://www.partow.net/programming/hashfunctions/index.html * #* * #* Copyright notice: * #* Free use of the General Purpose Hash Function Algorithms Library is * #* permitted under the guidelines and in accordance with the MIT License. * #* http://www.opensource.org/licenses/MIT * #* * #************************************************************************** # def rs_hash(key): a = 378551 b = 63689 hash_value = 0 for i in range(len(key)): hash_value = hash_value * a + ord(key[i]) a = a * b return hash_value def js_hash(key): hash_value = 1315423911 for i in range(len(key)): hash_value ^= ((hash_value << 5) + ord(key[i]) + (hash_value >> 2)) return hash_value def pjw_hash(key): bits_in_unsigned_int = 4 * 8 three_quarters = (bits_in_unsigned_int * 3) / 4 one_eighth = bits_in_unsigned_int / 8 high_bits = 0xFFFFFFFF << int(bits_in_unsigned_int - one_eighth) hash_value = 0 test = 0 for i in range(len(key)): hash_value = (hash_value << int(one_eighth)) + ord(key[i]) test = hash_value & high_bits if test != 0: hash_value = ((hash_value ^ (test >> int(three_quarters))) & (~high_bits)) return hash_value & 0x7FFFFFFF def elf_hash(key): hash_value = 0 for i in range(len(key)): hash_value = (hash_value << 4) + ord(key[i]) x = hash_value & 0xF0000000 if x != 0: hash_value ^= (x >> 24) hash_value &= ~x return hash_value def bkdr_hash(key): seed = 131 # 31 131 1313 13131 131313 etc.. hash_value = 0 for i in range(len(key)): hash_value = (hash_value * seed) + ord(key[i]) return hash_value def sdbm_hash(key): hash_value = 0 for i in range(len(key)): hash_value = ord(key[i]) + (hash_value << 6) + (hash_value << 16) - hash_value; return hash_value def djb_hash(key): hash_value = 5381 for i in range(len(key)): hash_value = ((hash_value << 5) + hash_value) + ord(key[i]) return hash_value def dek_hash(key): hash_value = len(key); for i in range(len(key)): hash_value = ((hash_value << 5) ^ (hash_value >> 27)) ^ ord(key[i]) return hash_value def bp_hash(key): hash_value = 0 for i in range(len(key)): hash_value = hash_value << 7 ^ ord(key[i]) return hash_value def fnv_hash(key): fnv_prime = 0x811C9DC5 hash_value = 0 for i in range(len(key)): hash_value *= fnv_prime hash_value ^= ord(key[i]) return hash_value def ap_hash(key): hash_value = 0xAAAAAAAA for i in range(len(key)): if (i & 1) == 0: hash_value ^= ((hash_value << 7) ^ ord(key[i]) * (hash_value >> 3)) else: hash_value ^= (~((hash_value << 11) + ord(key[i]) ^ (hash_value >> 5))) return hash_value
(2)布隆过滤器
import redis from GeneralHashFunctions import * class BloomFilterRedis(object): # 哈希哈数列表 hash_list = [rs_hash, js_hash, pjw_hash, elf_hash, bkdr_hash, sdbm_hash, djb_hash, dek_hash] def __init__(self, key, host='127.0.0.1', port=6379, hash_list=hash_list): self.key = key self.redis = redis.StrictRedis(host=host, port=port, charset='utf-8') self.hash_list = hash_list def random_generator(self, hash_value): ''' 将hash函数得出的函数值映射到[0, 2^32-1]区间内 ''' return hash_value % (1 << 32) def do_filter(self, item, save=True): ''' 过滤,判断是否存在 :param item: :param value: :return: ''' flag = True # 设置默认存在 for hash in self.hash_list: # 计算哈希值 hash_value = hash(item) # 获取映射到位数组的下标值 index_value = self.random_generator(hash_value) # 判断指定位置标记是否为0 if self.redis.getbit(self.key, index_value) == 0: if save: self.redis.setbit(self.key, index_value, 1) flag = False return flag if __name__ == '__main__': bloom = BloomFilterRedis("bloom_url") ret = bloom.do_filter("http://www.baidu2.com") print(ret) ret = bloom.do_filter("http://www.baidu.com") print(ret) ret = bloom.do_filter("http://www.baidu3.com1") print(ret)