布隆过滤器
先讲个使用场景,比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?
你会想到服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。问题是当用户量很大,每个用户看过的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上跟的上么?
实际上,如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行 exists 查询,当系统并发量很高时,数据库是很难扛住压力的。
你可能又想到了缓存,但是如此多的历史记录全部缓存起来,那得浪费多大存储空间啊?而且这个存储空间是随着时间线性增长,你撑得住一个月,你能撑得住几年么?但是不缓存的话,性能又跟不上,这该怎么办?
这时,布隆过滤器 (Bloom Filter) 闪亮登场了,它就是专门用来解决这种去重问题的。它在起到去重的同时,在空间上还能节省 90% 以上,只是稍微有那么点不精确,也就是有一定的误判概率。
其他场景
网页黑名单,
垃圾邮件过滤,
爬虫url去重
布隆过滤器是什么?
布隆过滤器是一个神奇的数据结构,可以用来判断一个元素是否在一个集合中。很常用的一个功能是用来去重。在爬虫中常见的一个需求:目标网站 URL 千千万,怎么判断某个 URL 爬虫是否宠幸过?简单点可以爬虫每采集过一个 URL,就把这个 URL 存入数据库中,每次一个新的 URL 过来就到数据库查询下是否访问过。
但是随着爬虫爬过的 URL 越来越多,每次请求前都要访问数据库一次,并且对于这种字符串的 SQL 查询效率并不高。除了数据库之外,使用 Redis 的 set 结构也可以满足这个需求,并且性能优于数据库。但是 Redis 也存在一个问题:耗费过多的内存。这个时候布隆过滤器就很横的出场了:
相比于数据库和 Redis,使用布隆过滤器可以很好的避免性能和内存占用的问题。
布隆过滤器本质是一个位数组,位数组就是数组的每个元素都只占用 1 bit 。每个元素只能是 0 或者 1。这样申请一个 10000 个元素的位数组只占用 10000 / 8 = 1250 B 的空间。布隆过滤器除了一个位数组,还有 K 个哈希函数。当一个元素加入布隆过滤器中的时候,会进行如下操作:
使用 K 个哈希函数对元素值进行 K 次计算,得到 K 个哈希值。
根据得到的哈希值,在位数组中把对应下标的值置为 1。
举个🌰,假设布隆过滤器有 3 个哈希函数:f1, f2, f3 和一个位数组 arr。现在要把 https://jaychen.cc 插入布隆过滤器中:
对值进行三次哈希计算,得到三个值 n1, n2, n3。
把位数组中三个元素 arr[n1], arr[n2], arr[3] 置为 1。
当要判断一个值是否在布隆过滤器中,对元素再次进行哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
看了上面的说明,必然会提出一个问题:当插入的元素原来越多,位数组中被置为 1 的位置就越多,当一个不在布隆过滤器中的元素,经过哈希计算之后,得到的值在位数组中查询,有可能这些位置也都被置为 1。这样一个不存在布隆过滤器中的也有可能被误判成在布隆过滤器中。但是如果布隆过滤器判断说一个元素不在布隆过滤器中,那么这个值就一定不在布隆过滤器中。简单来说:
布隆过滤器说某个元素在,可能会被误判。
布隆过滤器说某个元素不在,那么一定不在。
核心思想
BloomFilter的核心思想有两点:
多个hash,增大随机性,减少hash碰撞的概率
扩大数组范围,使hash值均匀分布,进一步减少hash碰撞的概率。
关于误判率
尽管BloomFilter已经尽可能的减小hash碰撞的概率了,但是,并不能彻底消除,因此正如上面提到的:
如果对应的bit位值都为1,那么也不能肯定这个url一定存在
也就是说,BloomFilter其实是存在一定的误判的,这个误判的概率显然和数组的大小以及hash函数的个数以及每个hash函数本身的好坏有关
维基百科 https://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives
下表是m与n比值在k个hash函数下面的误判率
m=二进制数组长度
n=元素个数
m/n | k | k=1 | k=2 | k=3 | k=4 | k=5 | k=6 | k=7 | k=8 |
2 | 1.39 | 0.393 | 0.400 | ||||||
3 | 2.08 | 0.283 | 0.237 | 0.253 | |||||
4 | 2.77 | 0.221 | 0.155 | 0.147 | 0.160 | ||||
5 | 3.46 | 0.181 | 0.109 | 0.092 | 0.092 | 0.101 | |||
6 | 4.16 | 0.154 | 0.0804 | 0.0609 | 0.0561 | 0.0578 | 0.0638 | ||
7 | 4.85 | 0.133 | 0.0618 | 0.0423 | 0.0359 | 0.0347 | 0.0364 | ||
8 | 5.55 | 0.118 | 0.0489 | 0.0306 | 0.024 | 0.0217 | 0.0216 | 0.0229 | |
9 | 6.24 | 0.105 | 0.0397 | 0.0228 | 0.0166 | 0.0141 | 0.0133 | 0.0135 | 0.0145 |
10 | 6.93 | 0.0952 | 0.0329 | 0.0174 | 0.0118 | 0.00943 | 0.00844 | 0.00819 | 0.00846 |
11 | 7.62 | 0.0869 | 0.0276 | 0.0136 | 0.00864 | 0.0065 | 0.00552 | 0.00513 | 0.00509 |
12 | 8.32 | 0.08 | 0.0236 | 0.0108 | 0.00646 | 0.00459 | 0.00371 | 0.00329 | 0.00314 |
13 | 9.01 | 0.074 | 0.0203 | 0.00875 | 0.00492 | 0.00332 | 0.00255 | 0.00217 | 0.00199 |
14 | 9.7 | 0.0689 | 0.0177 | 0.00718 | 0.00381 | 0.00244 | 0.00179 | 0.00146 | 0.00129 |
15 | 10.4 | 0.0645 | 0.0156 | 0.00596 | 0.003 | 0.00183 | 0.00128 | 0.001 | 0.000852 |
16 | 11.1 | 0.0606 | 0.0138 | 0.005 | 0.00239 | 0.00139 | 0.000935 | 0.000702 | 0.000574 |
17 | 11.8 | 0.0571 | 0.0123 | 0.00423 | 0.00193 | 0.00107 | 0.000692 | 0.000499 | 0.000394 |
18 | 12.5 | 0.054 | 0.0111 | 0.00362 | 0.00158 | 0.000839 | 0.000519 | 0.00036 | 0.000275 |
19 | 13.2 | 0.0513 | 0.00998 | 0.00312 | 0.0013 | 0.000663 | 0.000394 | 0.000264 | 0.000194 |
20 | 13.9 | 0.0488 | 0.00906 | 0.0027 | 0.00108 | 0.00053 | 0.000303 | 0.000196 | 0.00014 |
21 | 14.6 | 0.0465 | 0.00825 | 0.00236 | 0.000905 | 0.000427 | 0.000236 | 0.000147 | 0.000101 |
22 | 15.2 | 0.0444 | 0.00755 | 0.00207 | 0.000764 | 0.000347 | 0.000185 | 0.000112 | 7.46e-05 |
23 | 15.9 | 0.0425 | 0.00694 | 0.00183 | 0.000649 | 0.000285 | 0.000147 | 8.56e-05 | 5.55e-05 |
24 | 16.6 | 0.0408 | 0.00639 | 0.00162 | 0.000555 | 0.000235 | 0.000117 | 6.63e-05 | 4.17e-05 |
25 | 17.3 | 0.0392 | 0.00591 | 0.00145 | 0.000478 | 0.000196 | 9.44e-05 | 5.18e-05 | 3.16e-05 |
26 | 18 | 0.0377 | 0.00548 | 0.00129 | 0.000413 | 0.000164 | 7.66e-05 | 4.08e-05 | 2.42e-05 |
27 | 18.7 | 0.0364 | 0.0051 | 0.00116 | 0.000359 | 0.000138 | 6.26e-05 | 3.24e-05 | 1.87e-05 |
28 | 19.4 | 0.0351 | 0.00475 | 0.00105 | 0.000314 | 0.000117 | 5.15e-05 | 2.59e-05 | 1.46e-05 |
29 | 20.1 | 0.0339 | 0.00444 | 0.000949 | 0.000276 | 9.96e-05 | 4.26e-05 | 2.09e-05 | 1.14e-05 |
30 | 20.8 | 0.0328 | 0.00416 | 0.000862 | 0.000243 | 8.53e-05 | 3.55e-05 | 1.69e-05 | 9.01e-06 |
31 | 21.5 | 0.0317 | 0.0039 | 0.000785 | 0.000215 | 7.33e-05 | 2.97e-05 | 1.38e-05 | 7.16e-06 |
32 | 22.2 | 0.0308 | 0.00367 | 0.000717 | 0.000191 | 6.33e-05 | 2.5e-05 | 1.13e-05 | 5.73e-06 |
redis中的应用
Redis官方提供的布隆过滤器到4.0提供插件功能后才正式登场,布隆过滤器作为一个插件加载到Redis Server中,给Redis提供了强大的布隆去重功能。
git clone git: //github.com/RedisLabsModules/rebloom cd rebloom make # 在当前路径下生成rebloom.so文件 redis-server --loadmodule /path/to/rebloom.so # 启动redis服务器并使用前面生成的文件,或者在配置文件中添加 "loadmodule /path/to/rebloom.so" |
基本使用
布隆过滤器有两个基本指令,bf.add添加元素,bf.exists查询元素是否存在,bf.madd一次添加多个元素,bf.mexists一次查询多个元素。
127.0.0.1:6379> bf.add codehole user1 (integer) 1 127.0.0.1:6379> bf.add codehole user2 (integer) 1 127.0.0.1:6379> bf.add codehole user3 (integer) 1 127.0.0.1:6379> bf.exists codehole user1 (integer) 1 127.0.0.1:6379> bf.exists codehole user2 (integer) 1 127.0.0.1:6379> bf.exists codehole user3 (integer) 1 127.0.0.1:6379> bf.exists codehole user4 (integer) 0 127.0.0.1:6379> bf.madd codehole user4 user5 user6 1) (integer) 1 2) (integer) 1 3) (integer) 1 127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7 1) (integer) 1 2) (integer) 1 3) (integer) 1 4) (integer) 0 |
布隆过滤器在第一次add的时候自动创建基于默认参数的过滤器,Redis还提供了自定义参数的布隆过滤器。
在add之前使用bf.reserve指令显式创建,其有3个参数,key,error_rate, initial_size,错误率越低,需要的空间越大,error_rate表示预计错误率,initial_size参数表示预计放入的元素数量,当实际数量超过这个值时,误判率会上升,所以需要提前设置一个较大的数值来避免超出。默认的error_rate是0.01,initial_size是100。
注意事项
initial_size估计的过大会浪费存储空间,因此在使用前要尽可能精确估计好元素数量+冗余空间。
error_rate越小,需要的存储空间越大。
使用时不要让实际元素远大于初始化大小,当实际元素开始超出初始化大小时,应该对布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进去 (这就要求我们在其它的存储器中记录所有的历史元素)。
因为 error_rate 不会因为数量超出就急剧增加,这就给我们重建过滤器提供了较为宽松的时间。
- 当错误率为10%时,倍数比为2时,错误率接近40%;
- 当错误率为1%,倍数比为2时,错误率15%;
- 当错误率为0.1%,倍数为2时,错误率5%。
Resdis 4.0之前怎么用布隆过滤器
php+Redis实现的布隆过滤器
由于Redis实现了setbit和getbit操作,天然适合实现布隆过滤器
首先定义一个hash函数集合类,这些hash函数不一定都用到,实际上32位hash值的用3个就可以了,具体的数量可以根据你的位序列总量和你需要存入的量决定
class BloomFilterHash { /** * 由Justin Sobel编写的按位散列函数 */ public function JSHash( $string , $len = null) { $hash = 1315423911; $len || $len = strlen ( $string ); for ( $i =0; $i < $len ; $i ++) { $hash ^= (( $hash << 5) + ord( $string [ $i ]) + ( $hash >> 2)); } return ( $hash % 0xFFFFFFFF) & 0xFFFFFFFF; } /** * 该哈希算法基于AT&T贝尔实验室的Peter J. Weinberger的工作。 * Aho Sethi和Ulman编写的“编译器(原理,技术和工具)”一书建议使用采用此特定算法中的散列方法的散列函数。 */ public function PJWHash( $string , $len = null) { $bitsInUnsignedInt = 4 * 8; //(unsigned int)(sizeof(unsigned int)* 8); $threeQuarters = ( $bitsInUnsignedInt * 3) / 4; $oneEighth = $bitsInUnsignedInt / 8; $highBits = 0xFFFFFFFF << (int) ( $bitsInUnsignedInt - $oneEighth ); $hash = 0; $test = 0; $len || $len = strlen ( $string ); for ( $i =0; $i < $len ; $i ++) { $hash = ( $hash << (int) ( $oneEighth )) + ord( $string [ $i ]); } $test = $hash & $highBits ; if ( $test != 0) { $hash = (( $hash ^ ( $test >> (int)( $threeQuarters ))) & (~ $highBits )); } return ( $hash % 0xFFFFFFFF) & 0xFFFFFFFF; } /** * 类似于PJW Hash功能,但针对32位处理器进行了调整。它是基于UNIX的系统上的widley使用哈希函数。 */ public function ELFHash( $string , $len = null) { $hash = 0; $len || $len = strlen ( $string ); for ( $i =0; $i < $len ; $i ++) { $hash = ( $hash << 4) + ord( $string [ $i ]); $x = $hash & 0xF0000000; if ( $x != 0) { $hash ^= ( $x >> 24); } $hash &= ~ $x ; } return ( $hash % 0xFFFFFFFF) & 0xFFFFFFFF; } /** * 这个哈希函数来自Brian Kernighan和Dennis Ritchie的书“The C Programming Language”。 * 它是一个简单的哈希函数,使用一组奇怪的可能种子,它们都构成了31 .... 31 ... 31等模式,它似乎与DJB哈希函数非常相似。 */ public function BKDRHash( $string , $len = null) { $seed = 131; # 31 131 1313 13131 131313 etc.. $hash = 0; $len || $len = strlen ( $string ); for ( $i =0; $i < $len ; $i ++) { $hash = (int) (( $hash * $seed ) + ord( $string [ $i ])); } return ( $hash % 0xFFFFFFFF) & 0xFFFFFFFF; } /** * 这是在开源SDBM项目中使用的首选算法。 * 哈希函数似乎对许多不同的数据集具有良好的总体分布。它似乎适用于数据集中元素的MSB存在高差异的情况。 */ public function SDBMHash( $string , $len = null) { $hash = 0; $len || $len = strlen ( $string ); for ( $i =0; $i < $len ; $i ++) { $hash = (int) (ord( $string [ $i ]) + ( $hash << 6) + ( $hash << 16) - $hash ); } return ( $hash % 0xFFFFFFFF) & 0xFFFFFFFF; } /** * 由Daniel J. Bernstein教授制作的算法,首先在usenet新闻组comp.lang.c上向世界展示。 * 它是有史以来发布的最有效的哈希函数之一。 */ public function DJBHash( $string , $len = null) { $hash = 5381; $len || $len = strlen ( $string ); for ( $i =0; $i < $len ; $i ++) { $hash = (int) (( $hash << 5) + $hash ) + ord( $string [ $i ]); } return ( $hash % 0xFFFFFFFF) & 0xFFFFFFFF; } /** * Donald E. Knuth在“计算机编程艺术第3卷”中提出的算法,主题是排序和搜索第6.4章。 */ public function DEKHash( $string , $len = null) { $len || $len = strlen ( $string ); $hash = $len ; for ( $i =0; $i < $len ; $i ++) { $hash = (( $hash << 5) ^ ( $hash >> 27)) ^ ord( $string [ $i ]); } return ( $hash % 0xFFFFFFFF) & 0xFFFFFFFF; } /** */ public function FNVHash( $string , $len = null) { $prime = 16777619; //32位的prime 2^24 + 2^8 + 0x93 = 16777619 $hash = 2166136261; //32位的offset $len || $len = strlen ( $string ); for ( $i =0; $i < $len ; $i ++) { $hash = (int) ( $hash * $prime ) % 0xFFFFFFFF; $hash ^= ord( $string [ $i ]); } return ( $hash % 0xFFFFFFFF) & 0xFFFFFFFF; } } |
使用redis来操作
/** * 使用redis实现的布隆过滤器 */ abstract class BloomFilterRedis { /** * 需要使用一个方法来定义bucket的名字 */ protected $bucket ; protected $hashFunction ; public function __construct( $config , $id ) { if (! $this ->bucket || ! $this ->hashFunction) { throw new Exception( "需要定义bucket和hashFunction" , 1); } $this ->Hash = new BloomFilterHash; $this ->Redis = new YourRedis; //假设这里你已经连接好了 } /** * 添加到集合中 */ public function add( $string ) { $pipe = $this ->Redis->multi(); foreach ( $this ->hashFunction as $function ) { $hash = $this ->Hash-> $function ( $string ); $pipe ->setBit( $this ->bucket, $hash , 1); } return $pipe -> exec (); } /** * 查询是否存在, 存在的一定会存在, 不存在有一定几率会误判 */ public function exists( $string ) { $pipe = $this ->Redis->multi(); $len = strlen ( $string ); foreach ( $this ->hashFunction as $function ) { $hash = $this ->Hash-> $function ( $string , $len ); $pipe = $pipe ->getBit( $this ->bucket, $hash ); } $res = $pipe -> exec (); foreach ( $res as $bit ) { if ( $bit == 0) { return false; } } return true; } } |
上面定义的是一个抽象类,如果要使用,可以根据具体的业务来使用。比如下面是一个过滤重复内容的过滤器。
/** * 重复内容过滤器 * 该布隆过滤器总位数为2^32位, 判断条数为2^30条. hash函数最优为3个.(能够容忍最多的hash函数个数) * 使用的三个hash函数为 * BKDR, SDBM, JSHash * * 注意, 在存储的数据量到2^30条时候, 误判率会急剧增加, 因此需要定时判断过滤器中的位为1的的数量是否超过50%, 超过则需要清空. */ class FilteRepeatedComments extends BloomFilterRedis { /** * 表示判断重复内容的过滤器 * @var string */ protected $bucket = 'rptc' ; protected $hashFunction = array ( 'BKDRHash' , 'SDBMHash' , 'JSHash' ); } |