有道无术,术尚可求,有术无道,止于术。
本系列Redis 版本 7.2.5
1. 布隆过滤器
1.1 概述
在需要判断某个元素是否在集合中时,可以使用链表、散列表将所有元素保存起来,然后通过比较进行判断。但是随着集合中元素的增加,需要的存储空间也会呈现线性增长,最终达到瓶颈,同时检索速度也越来越慢。
1970
年,Howard Bloom
提供出了布隆过滤器(Bloom Filter
),是一种概率性数据结构,主要用于检测一个元素是否属于某个集合。
1.2 优点
1、占用空间少
不存储元素本身,仅存储哈希结果取模运算后的位标记,相比其他数据结构能节省大量空间。在这个免费网站上,误判率 3%
,4
个哈希函数,10
亿个元素只占用 900MB
左右的空间:
2、插入、检查时间快
单个布隆过滤器中,插入和检查元素的时间复杂度是 O(k)
,其中 k
为哈希函数的个数,这个值通常比较小。在处理大量数据时,布隆过滤器的查询速度非常快。
3、保密性强
不存储数据本身,适合某些需要保密的场景。
1.3 缺点
1、存在误判率
而哈希函数无法完全避免哈希冲突,多个不同的元素可能通过哈希函数映射到位数组的相同位置,并导致这些位置被设置为 1
。当检查一个实际上未被添加到过滤器的元素时,可能由于哈希值一样,导致错误的判断该元素已存在(实际不存在)。
误判率可以通过调整参数(如哈希函数的个数、布隆过滤器的大小)来降低,但无法完全避免。
存在误判但绝不会有漏判
2、无法删除
布隆过滤器的位数组中只能将元素对应的位设置为1
,不能设置为0
,因此一旦元素被加入布隆过滤器,就无法从中删除。
3、不支持获取元素
不存储数据本身,只能判断元素是否存在,但无法获取元素本身的值。
4、不支持获取计数
同一个元素可以多次插入布隆过滤器,但效果和插入一次相同,布隆过滤器无法统计元素的出现次数。
5、性能随容量变化
当布隆过滤器的容量快满时,哈希碰撞的概率会变大,插入和查询的错误率也会随之增加。
2. 工作原理
2.1 数据结构
布隆过滤器由位数组和哈希函数组成:
- 位数组:布隆过滤器由一个很长的二进制向量(或称为位数组)组成,数组中的每个元素都只占用
1 bit
,其值只能是0
或1
。 - 哈希函数:布隆过滤器使用多个哈希函数,这些哈希函数将输入元素映射到位数组中的不同位置。
2.2 添加
添加时,会通过多个(K
)哈希函数将该元素映射到位数组中的个多个(K
)点上,并将这些点置为1
:
2.3 检查
当查询一个元素是否存在于布隆过滤器中时,也通过同样的 K
个哈希函数将该元素映射到位数组中的 K
个点上。
如果其中有任何一个点不是1
,则确定该元素一定不存在于布隆过滤器中。如果所有点都是1
,则认为存在该元素(存在误判率)。
2.4 删除
布隆过滤器不支持删除操作,因为删除一个元素可能会影响其他元素的判断结果。布隆过滤器衍生了很多变种,可以支持删除操作,例如布谷鸟过滤器(Cuckoo Filter
)。
布谷鸟过滤器参考文档:
3. 案例演示
很多框架都实现了 Bloom Filter
,比如 Guava
、Hutools
、Redisson
,这里是学习 Redis
,所以还是使用 Redis
中的布隆过滤器。
RedisBloom 是一个流行的 Redis 模块,提供了对布隆过滤器的支持。安装了这个模块后,你可以使用它来创建、添加元素到布隆过滤器、检查元素是否存在以及执行其他相关操作
3.1 安装
Redis
从 4.0
版本开始,通过 Redis
模块支持了布隆过滤器。这里需要以模块的方式额外引入 RedisBloom
。在 RedisBloom 中,给出了相关安装说明:
使用压缩包进行安装的话,需要安装 Python 3
,然后编译,再在 redis.conf
文件中引入,这里尝试了下,各种报错,没有安装成功。所以这里直接使用 Docker
安装 Redis Stack
(提供了 RedisBloom
)。
使用 daocloud
地址拉取镜像:
bash
[root@localhost RedisBloom]# docker pull m.daocloud.io/docker.io/redis/redis-stack-server:latest
docker run
直接运行:
bash
[root@localhost RedisBloom]# docker run -p 6379:6379 -it --rm m.daocloud.io/docker.io/redis/redis-stack-server:latest
启动界面:
3.2 常用命令
所有命令:
命名 | 描述 |
---|---|
BF.ADD | 向布隆过滤器中添加一个元素 |
BF.CARD | 返回布隆过滤器的基数 |
BF.EXISTS | 检查一个元素是否可能存在于布隆过滤器中 |
BF.INFO | 获取布隆过滤器的信息,如当前的元素数量、错误率等 |
BF.INSERT | 使用指定的错误率、容量和扩展量创建一个新的布隆过滤器 |
BF.LOADCHUNK | 使用 BF.SCANDUMP 命令之前保存的布隆过滤器进行恢复 |
BF.MADD | 向布隆过滤器中一次性添加多个元素 |
BF.MEXISTS | 检查多个元素是否可能存在于布隆过滤器中 |
BF.RESERVE | 创建一个新的布隆过滤器,并指定其预期的元素数量和错误率 |
BF.SCANDUMP | 布隆过滤器的增量保存 |
3.2.1 BF.ADD
向布隆过滤器中添加一个元素。
语法格式:
java
BF.ADD key item
参数说明:
key
:布隆过滤器的名称item
:添加到布隆过滤器中的一个元素
示例:
bash
BloomFilter:0>BF.ADD myfilter aa
"1"
3.2.2 BF.INFO
获取布隆过滤器的信息,如当前的元素数量、错误率等。
语法格式:
java
BF.INFO key [CAPACITY | SIZE | FILTERS | ITEMS | EXPANSION]
参数说明:
key
:布隆过滤器的名称- 可选参数(未指定任何可选参数,则返回所有信息字段):
CAPACITY
:此布隆过滤器可以存储的唯一元素数量(包括已添加的元素)SIZE
:返回内存大小:为此布隆过滤器分配的字节数FILTERS
:返回子过滤器的数量ITEMS
:返回添加到此布隆过滤器中并被检测为唯一的元素数量(这些元素导致至少在一个子过滤器中至少设置了一个位)EXPANSION
:返回扩展率
示例:
bash
BloomFilter:0>BF.INFO myfilter
1) "Capacity"
2) "100"
3) "Size"
4) "240"
5) "Number of filters"
6) "1"
7) "Number of items inserted"
8) "1"
9) "Expansion rate"
10) "2"
3.2.3 BF.RESERVE
创建一个空的布隆过滤器,该过滤器具有一个初始指定的容量的单一子过滤器,并设定了一个上限错误率(error_rate
)。
默认情况下,当达到容量时,过滤器会通过创建额外的子过滤器来自动扩展。新的子过滤器的大小是前一个子过滤器的大小乘以扩展率(expansion
)得出的。
尽管过滤器可以通过创建子过滤器来扩展,但建议预留估计所需的容量,因为维护和查询子过滤器需要额外的内存(每个子过滤器使用额外的位和哈希函数),并且比在创建时具有正确容量的等效过滤器消耗更多的CPU
时间。
语法格式:
java
BF.RESERVE key error_rate capacity [EXPANSION expansion]
[NONSCALING]
必填参数:
key
:布隆过滤器的名称error_rate
:是期望的误报率。该比率是一个介于0
和1
之间的小数值。例如,如果期望的误报率为0.1%
(即1000
个中有一个),则error_rate
应设置为0.001
。capacity
:计划添加到过滤器中的元素数量。如果您的过滤器允许扩展,则在添加超过此数量的更多元素后,性能将开始下降。实际的性能下降程度取决于超出限制的程度。性能随着子过滤器数量的增加而线性下降。
可选参数:
NONSCALING
:如果达到初始容量,则阻止过滤器创建额外的子过滤器。非扩展型过滤器比其可扩展的对应物稍微节省一些内存。当达到容量时,过滤器会返回一个错误。EXPANSION expansion
:当达到容量时,会创建一个额外的子过滤器。新子过滤器的大小是最后一个子过滤器的大小乘以扩展率(expansion
),扩展率指定为一个正整数。默认值是2
。
示例:
bash
BloomFilter:0>BF.RESERVE bf 0.01 1000
"OK"
BloomFilter:0>BF.RESERVE bf 0.01 1000
"ERR item exists"
BloomFilter:0>BF.RESERVE bf_exp 0.01 1000 EXPANSION 2
"OK"
BloomFilter:0>BF.RESERVE bf_non 0.01 1000 NONSCALING
"OK"
BloomFilter:0>
3.2.4 BF.EXISTS
确定给定的元素是否已添加到布隆过滤器中。
语法格式:
java
BF.EXISTS key item
返回值:
- 整数:其中
1
表示以高概率项已被添加到过滤器中,0
表示键不存在或尚未被添加到过滤器中。 - 空数组
[]
:在出现错误(无效参数、错误的键类型等)时返回。
示例:
bash
BloomFilter:0>BF.ADD bf item1
"1"
BloomFilter:0>BF.EXISTS bf item1
"1"
BloomFilter:0>BF.EXISTS bf item2
"0"
3.4 客户端连接
RedisBloom
支持以下客户端进行连接:
使用 Jedis
客户端示例:
bash
public static void main(String[] args) {
UnifiedJedis jedis = new UnifiedJedis("redis://192.168.56.101:6379");
// 创建
String res1 = jedis.bfReserve("bikes:models", 0.01, 1000);
System.out.println(res1); // >>> OK
// 添加
boolean res2 = jedis.bfAdd("bikes:models", "Smoky Mountain Striker");
System.out.println(res2); // >>> True
// 判断是否存在
boolean res3 = jedis.bfExists("bikes:models", "Smoky Mountain Striker");
System.out.println(res3); // >>> True
}
4. 应用场景
在软件领域,当数据量很大时,可以使用布隆过滤器检测一个元素是否属于某个集合。
4.1 缓存穿透防护
缓存穿透场景中,一些恶意的请求会故意大量查询不存在的 key
,会直接穿透到数据库,对数据库造成巨大压力。
可以使用布隆过滤器预先判断 key
是否已存在,如果存在,才调用数据库或者缓存查询。
4.2 敏感词、黑名单、垃圾邮箱....
可以使用布隆过滤器判断敏感词、黑名单、垃圾邮箱是否存在,存在则执行拦截。