背景介绍
在互联网中,我们经常遇到需要在大量数据中判断目标数据是否存在的情况。例如,在网络爬虫中,我们需要判断某个网址是否已经被访问过。为了实现这一功能,通常需要使用一个容器来存储已访问过的网址。如果将这些数据直接存储在磁盘中,每次判断都要进行磁盘查询,这将导致大量的IO操作,效率较低。因此,我们希望将这些数据保存在内存中。在数据量较小的情况下,可以使用Redis来存储这些数据。但是,当数据量超过上千万时,将会消耗几GB甚至几十GB的内存空间。然而,对于仅需要记录数据是否存在的情况而言,这样使用大量内存显然是浪费的。为了解决这个问题,我们可以使用布隆过滤器(Bloom Filter)。布隆过滤器是一种占用空间少且时间效率高的工具。
布隆过滤器原理
什么是布隆过滤器?
布隆过滤器是一种空间效率高和时间效率高的概率型数据结构,用于判断某个元素是否在一个集合中。它通过多个哈希函数将元素映射到一个位数组中,虽然能快速判断元素是否存在,但有一定的误判率,即可能会误认为元素存在,但不会漏掉实际存在的元素。
简单点,你可以认为,布隆过滤器确实是一个很长的二进制向量(位数组)和一系列哈希函数的组合~
布隆过滤器的原理很简单:
当一个元素被添加到集合中时,它会通过m个哈希函数映射到位数组中的m个位置,并将这些位置的值设置为 1。在检查元素是否在集合中时,检查这些位置是否全为 1。如果其中有任何一个位置为 0,则该元素一定不在集合中;如果所有位置均为 1,则该元素可能在集合中。
举个例子:
假设现在有3个哈希函数,和一个8位的bit数组。元素a和b,都经过三次哈希函数生成三个哈希值,并映射到位数组的不同的位置,并设置为1。元素a映射的位置是(0,3,7),元素b映射的位置是(2,5,7).
如果一个元素c过来,我们检查它映射后的三个位置是否全是1,就可以判断元素C是否在当前集合中了。
其实我们可以发现,元素a和元素b映射的位置7都是1,也就是说,位置是可能重叠的。假设当前集合已经有a和b了,但是呢一个元素c过来,它映射的位置为(0,2,7),这时候,它的所有位置都是1,布隆过滤器是认为它可能在集合中,但是我们看到元素c是不在当前集合中的。
也是就说,布隆过滤器是可能存在误判的,通俗点说就是假阳性。
布隆过滤器的优点和缺点
布隆过滤器的缺点,其实就是,存在一定误判。
要降低误判率的话,我们可以:
-
增加位数组的大小:增大位数组的长度可以减少每个位的填充率,从而降低假阳性率。较大的位数组可以容纳更多的元素,并且哈希值的碰撞几率减少。
-
增加哈希函数的数量:使用更多的哈希函数可以进一步分散哈希映射,降低假阳性率。
-
使用分级布隆过滤器:在布隆过滤器前使用一个较小的布隆过滤器作为初步过滤器,只有在初步过滤器确认元素可能存在时,才使用主要布隆过滤器,这可以减少对主要布隆过滤器的查询次数。
而布隆过滤器的优点也很明确,它的空间效率和查询效率,相对与其他一般算法,都有明显优势。正所谓鱼和熊掌,不可兼得,但是它居然不一样,空间和时间效率都不错!
确切地说,布隆过滤器的高效和低空间复杂度归功于其使用的哈希函数和位数组。它利用多个哈希函数将数据映射到一个固定大小的位数组中,减少了需要存储的数据量,从而降低了空间复杂度。由于它是基于位操作的,查找操作也非常快,这使得它在处理大量数据时表现出高效性
布隆过滤器的经典使用场景
缓存穿透
布隆过滤器,常常跟我们讨论的缓存穿透出现。它是最经典的使用场景。
缓存穿透保护:在分布式缓存系统中,布隆过滤器可以用来防止缓存穿透。通过在缓存前面加一个布隆过滤器,可以有效地阻止不存在的数据请求直接到达后端数据库,从而减少数据库负载和提高系统性能。
恶意网址检测
布隆过滤器可以用于网络安全中,例如检测恶意网址。通过将已知的恶意网址存储在布隆过滤器中,可以快速判断一个网址是否可能是恶意的。
防止消息重复消费
防止消息重复消费:布隆过滤器还可以用于防止MQ消息重复消费。比如说,我们在发送消息时可以对每个消息设置唯一的key,然后呢,在消费者消费处理时,利用布隆过滤器对消息的key检索。如果不存在则进行消费,然后插入布隆过滤器,如果存在则说明消息已经消费过。
但是呢,由于布隆过滤的假阳性,一般要求集合数据库存储已处理消息ID,来进一步减少重复消费的风险。
在消息量大、内存空间有限的情况下,使用布隆过滤器是个很不错的选择。
布隆过滤器可以用于网络安全中,例如检测恶意网址。通过将已知的恶意网址存储在布隆过滤器中,可以快速判断一个网址是否可能是恶意的。
使用 Google Guava库中的布隆过滤器
使用Google Guava库中的布隆过滤器(Bloom Filter)功能展示如何创建一个布隆过滤器,并如何使用它来检查元素是否可能存在于集合中。
-
在
pom.xml
中添加依赖:
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version> <!-- Use the latest version -->
</dependency>
</dependencies>
-
编写Java代码来创建和使用布隆过滤器:
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterExample {
public static void main(String[] args) {
// 初始化布隆过滤器
// 预期插入的元素数量
long expectedInsertions = 10000L;
// 期望的误报率
double fpp = 0.03; // 3%的误报率
// 使用Funnels.stringFunnel()来处理String类型的数据
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), expectedInsertions, fpp);
// 插入元素
bloomFilter.put("element1");
bloomFilter.put("element2");
// ... 可以继续插入其他元素
// 检查元素是否可能存在于集合中
boolean mightContainElement1 = bloomFilter.mightContain("element1");
System.out.println("Element1 might be present: " + mightContainElement1); // 应该输出 true
boolean mightContainElement3 = bloomFilter.mightContain("element3");
System.out.println("Element3 might be present: " + mightContainElement3); // 可能输出 true 或 false,取决于误报率
// 注意:布隆过滤器可能会产生误报(false positives),但不会产生误报(false negatives)
// 如果 mightContain 返回 false,那么该元素肯定不在集合中
// 如果 mightContain 返回 true,那么该元素可能在集合中,但也可能不在
}
}
使用 Redisson中的布隆过滤器
使用 Redisson 创建布隆过滤器,插入元素,并检查某个元素是否存在。
-
在
pom.xml
文件中添加 Redisson 依赖:
<dependencies>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.1</version> <!-- 使用最新版本 -->
</dependency>
</dependencies>
-
编写代码来创建布隆过滤器,插入元素,并检查元素:
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonBloomFilterExample {
public static void main(String[] args) {
// 配置 Redisson 客户端
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379"); // 替换为你的 Redis 服务器地址
// 创建 Redisson 客户端实例
RedissonClient redisson = Redisson.create(config);
// 创建布隆过滤器
// 注意:这里的名称 "myBloomFilter" 是布隆过滤器的唯一标识,你可以根据需要更改
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("myBloomFilter");
// 初始化布隆过滤器,设置预期插入的元素数量和误报率
bloomFilter.tryInit(10000L, 0.03);
// 插入元素
bloomFilter.add("码");
bloomFilter.add("道");
// 查找元素
boolean mightContainYi = bloomFilter.contains("易");
System.out.println("布隆过滤器中可能包含'易':" + mightContainYi);
// 关闭 Redisson 客户端
redisson.shutdown();
}
}
注意:由于布隆过滤器的特性,
contains
方法返回true
并不意味着元素一定存在,而返回false
则意味着元素一定不存在。
对于 bloomFilter.tryInit(10000L, 0.03);
的参数设置,应根据实际业务给出。