神器的布隆过滤器

什么是布隆过滤器

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”

相比于传统的 ListSetMap 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

实现原理

HashMap 的问题

讲述布隆过滤器的原理之前,我们先思考一下,通常你判断某个元素是否存在用的是什么?应该蛮多人回答 HashMap 吧,确实可以将值映射到 HashMapKey,然后可以在 O(1) 的时间复杂度内返回结果,效率奇高。但是 HashMap 的实现也有缺点,例如存储容量占比高,考虑到负载因子的存在,通常空间是不能被用满的,而一旦你的值很多例如上亿的时候,那 HashMap 占据的内存大小就变得很可观了。

还比如说你的数据集存储在远程服务器上,本地服务接受输入,而数据集非常大不可能一次性读进内存构建 HashMap 的时候,也会存在问题。

布隆过滤器数据结构

布隆过滤器是一个 bit 向量或者说 bit 数组,长这样:

http://static.cyblogs.com/QQ20200618-210117@2x.jpg

数组里面的值就只会存在truefalse

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值则上图转变为:

http://static.cyblogs.com/QQ20200618-210604@2x.jpg

而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回图中的3bit位,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置为了 1 ,那么程序还是会判断 “taobao” 这个值存在。

支持删除么

传统的布隆过滤器并不支持删除操作。但是名为 Counting Bloom filter 的变种可以用来测试元素计数个数是否绝对小于某个阈值,它支持元素删除。可以参考文章 Counting Bloom Filter 的原理和实现:https://cloud.tencent.com/developer/article/1136056

Guava里的布隆过滤器:com.google.common.hash.BloomFilter

1
2
3
4
5
6
7
8
9
// 可能存在
public boolean mightContain(T object) {
return strategy.mightContain(object, funnel, numHashFunctions, bits);
}

// 放入值
public boolean put(T object) {
return strategy.put(object, funnel, numHashFunctions, bits);
}

如何选择哈希函数个数和布隆过滤器长度

很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

http://static.cyblogs.com/QQ20200618-212220@2x.jpg

我们可以参考网站给的一个参考值:https://krisives.github.io/bloom-calculator/

http://static.cyblogs.com/QQ20200618-212520@2x.jpg

比如:我们有1000的数量,误判率是0.1,那么需要3.32hash函数,位的长度为4793

Redis-BloomFilter实践

Redis4.0版本推出了 module 的形式,可以将 module 作为插件额外实现Redis的一些功能。官网推荐了一个 RedisBloom 作为 Redis 布隆过滤器的 Module

RedisBloom需要先进行安装,推荐使用Docker进行安装,简单方便:

1
2
3
4
5
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

熟悉一下布隆过滤器基本指令:

  • bf.add 添加元素到布隆过滤器
  • bf.exists 判断元素是否在布隆过滤器
  • bf.madd 添加多个元素到布隆过滤器,bf.add只能添加一个
  • bf.mexists 判断多个元素是否在布隆过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

Redis Bitmap实现简单的布隆过滤器

BitmapRedis中并不是一个单独的数据类型,而是由字符串类型(Redis内部称Simple Dynamic String,SDS)之上定义的与比特相关的操作实现的,此时SDS就被当做位数组了。下面是在redis-cli中使用getbitsetbit指令的操作示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 字符串"meow"的二进制表示:01101101 01100101 01101111 01110111
es1:19000> set bitmap_cat "meow"
OK
# 最低位下标为0。取得第3位的比特(0)
es1:19000> getbit bitmap_cat 3
(integer) 0
# 取得第23位的比特(1)
es1:19000> getbit bitmap_cat 23
(integer) 1
# 将第7位设为0
es1:19000> setbit bitmap_cat 7 0
(integer) 1
# 将第14位设为1
es1:19000> setbit bitmap_cat 14 1
(integer) 0
# 修改过后的字符串变成了"lgow"
es1:19000> get bitmap_cat
"lgow"

RedisBitmap是自动扩容的,亦即get/set到高位时,就会主动填充0。此外,还有bitcount指令用于计算特定字节范围内1的个数,bitop指令用来执行位运算(支持andorxornot)。相应的用法可以查询Redis官方文档等。

参考地址

如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员简栈文化-小助手(lastpass4u),他会拉你们进群。

简栈文化服务订阅号