【全网最硬核布隆过滤器指南】万字长文从哈希碰撞本质出发,深度剖析布隆过滤器的位数组与多哈希函数核心机制,结合Google Guava与Redisson源码解读,手把手教你通过SpringBoot+Redis构建支持动态扩容的工业级布隆过滤器。本文不仅详解误判率公式推导与参数调优方法论,更包罗缓存穿透防护、十亿级数据去重等6大真实场景解决方案,最后通过JMeter压测验证过滤器性能,助你掌握这个让数据库查询性能翻倍的核心技术。随文附赠可动态扩容的布隆过滤器SpringBoot工程源码,技术深度与实战价值兼备!
1、什么是布隆过滤器?
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,由 Burton Howard Bloom 在1970年提出。它重要用于判定一个元素是否可能属于某个集合,而不支持直接获取集合中的所有元素。
布隆过滤器的根本结构是一个固定长度的位数组和一组哈希函数。
1.1 工作原理
- 初始化时,位数组的所有位都被设置为0。
- 当要插入一个元素时,使用预先设定好的多个独立、均匀分布的哈希函数对元素举行哈希运算,每个哈希函数都会计算出一个位数组的索引位置。
- 将通过哈希运算得到的每个索引位置的位设置为1。
- 查询一个元素是否存在时,同样用相同的哈希函数对该元素举行运算,并检查对应位数组的位置是否都是1。假如所有位都为1,则认为该元素可能存在于集合中;假如有任何一个位为0,则可以确定该元素肯定不在集合中。
- 由于哈希碰撞的存在,当多位同时为1时,可能出现误报(False Positive),即报告元素可能在集合中,但实际上并未被插入过。但布隆过滤器不会出现漏报(False Negative),即假如布隆过滤器说元素不在集合中,则这个结论是绝对正确的。
1.2 布隆过滤器的长处
- 空间效率高:相比于准确存储所有元素的数据结构,布隆过滤器所需的内存空间小得多。
- 查询速度快:只必要执行几个哈希函数并检查位数组即可完成查询。
如何判定这个数据的真实性?很简单,你在设置好参数初始化布隆过滤器时,它会初始化一个位数组,这个容量是固定的且一次性申请对应容量的内存。
颠末实际测试,根本与上述网站评估同等。另外和大家说个小 Tips,Redisson 布隆过滤器在 Redis 中是以字符串方式存储的,底层数据结构是 Redis 中的 BitSet(位图)。
1.3 缺点
- 不可删除:标准布隆过滤器不支持元素的删除操纵,由于无法得知哪些位仅是由于当前查询的元素而置1的。
- 误报率:随着元素数量增加,误报率也会逐渐升高,但是可以通过调整位数组巨细和哈希函数数量来控制误报率。
布隆过滤器广泛应用在数据库系统、网络爬虫去重、缓存穿透防护等场景中,尤其适合那些能容忍肯定误判率的应用。
2、布隆过滤器的使用场景
布隆过滤器因其高效的空间占用和快速查询本领,在很多大规模数据处理和高性能系统中得到广泛应用,以下是一些典型的使用场景:
- 缓存穿透防护:
- 在分布式缓存系统如Redis或Memcached中,用于制止缓存穿透题目。当一个请求试图访问数据库中的某个不存在的键时,假如直接去数据库查询会增加数据库压力。通过在前端摆设一个布隆过滤器,可以预先判定该键很可能不存在于数据库中,从而制止对数据库发起无效请求。
- 重复数据检测:
- 在爬虫抓取网页或者日志分析中,用于URL去重,确保不会重复抓取相同的页面或纪录。
- 在大数据处理中,好比在Hadoop等框架中,用来过滤掉重复的数据块或者纪录,减少计算和存储负担。
- 垃圾邮件过滤:
- 用于电子邮件系统的垃圾邮件地址库,快速判定收到的邮件是否可能来自已知的垃圾邮件发送者。
- 实时监控与报警系统:
- 当大量事件流经系统时,可以用于快速辨认并过滤出已知异常事件,低落报警系统误报率。
- 推荐系统:
- 在个性化推荐系统中,用于快速排除用户已经浏览过或者不感爱好的内容。
- 数据库索引优化:
- 对于大型数据库,可以利用布隆过滤器作为辅助索引结构,提前过滤掉大部门肯定不在结果会合的查询条件,减轻主索引的压力。
- 社交网络和互联网服务:
- 在社交网络中,用于快速检测用户上传的内容是否存在违规信息,或是检查用户ID、账号是否存在黑名单中。
- 数据分析与挖掘:
- 在大规模数据清洗阶段,用来剔除重复样本或无效数据。
- 网络安全:
- 网络防火墙和入侵检测系统中,用于过滤已知恶意IP或攻击特性。
总之,任何须要在高吞吐量下以肯定的错误容忍度举行元素存在性检测且内存资源有限的场景,都可以考虑使用布隆过滤器。
3、布隆过滤器的原理
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,重要用于判定一个元素是否可能属于某个集合。它的根本原理是利用位数组(Bitmap)和一组哈希函数来实现快速且近似的存在性查询。
3.1 布隆过滤器的数据结构
布隆过滤器(Bloom Filter)的数据结构重要由一个固定长度的位数组(Bit Array)和一组哈希函数(Hash Functions)组成。
位数组:
- 这是一个很长的二进制向量,所有位初始化为0。它的长度(m)是在创建布隆过滤器时根据预期要插入的元素数量、渴望的错误率以及所使用的哈希函数数量计算得出的。
哈希函数组:
- 布隆过滤器通常使用k个独立且均匀分布的哈希函数。每个函数都能将输入元素映射到位数组的一个索引上。
- 当插入一个元素时,通过这k个哈希函数分别计算出该元素对应的k个差别的位数组索引,并将这些位置上的位设置为1。
- 查询时,同样应用这k个哈希函数来确定查询元素可能所在的位数组中的位置,假如所有这些位置上的位都是1,则认为该元素可能存在(存在误报的概率),若至少有一位是0,则可以确定该元素不在集合中。
因此,布隆过滤器的核心数据结构实在相称简单,但其巧妙之处在于利用多个哈希函数对数据举行分散存储,并通过牺牲绝对准确性换取了空间效率和查询速度的优势。
3.2 初始化阶
- 创建一个固定长度的二进制向量,所有位都被初始化为0。
- 选择k个差别的哈希函数,每个哈希函数都能将输入的元素映射到位数组的一个索引位置上。
3.3 插入元素过程
- 当必要将一个元素添加到布隆过滤器中时,使用这k个哈希函数分别计算出这个元素对应的k个位数组的索引。
- 将这些索引位置上的位都设置为1。
例如,key = Liziba,无偏hash函数的个数k=3,分别为hash1、hash2、hash3。三个hash函数计算后得到三个数组下标值,并将其值修改为1.
3.4 查询元素是否存在
- 对于查询操纵,同样使用这k个哈希函数对目标元素举行哈希运算,得到相应的k个位数组索引。
- 检查这些索引位置上的位是否全为1。
- 假如有任何一个位置为0,则可以确定该元素肯定不在集合中(零错误率)。
- 假如所有位置都是1,则认为该元素可能存在在集合中(存在误报的概率,即False Positive)。
例如我们查询 “cunzai” 这个值是否存在,哈希函数返回了 1、5、8三个值
由于哈希碰撞的存在,在实际应用中,随着更多元素被插入,相同哈希值对应的位会被多次置1,这就导致本来未出现过的元素颠末哈希运算后也可能指向已经置1的位置,从而产生误报。不过,通过调整位数组巨细、哈希函数的数量以及负载因子等参数,可以在误报率和存储空间之间取得平衡。
总之,布隆过滤器提供了一种空间效率极高但牺牲了准确性的解决方案,特殊适实用于那些能够容忍肯定误报率的大规模数据处理场景。
3.5 元素删除
在标准的布隆过滤器(Bloom Filter)实现中,包括基于Java实现的RBloomFilter或其他变体,元素一旦被添加到布隆过滤器中,是不能被准确删除的。这是由于布隆过滤器的设计原理决定了它是一种空间效率极高的概率型数据结构,用来测试一个元素是否“可能”存在于集合中。当向布隆过滤器插入元素时,会使用多个哈希函数将元素映射到位数组的特定位置上置1。由于位数组中没有关联元素值的信息,以是无法通过逆操纵来还原或扫除单个元素。
因此,在Java或者其他任何编程语言中的布隆过滤器实现,通常不会提供直接删除元素的方法。假如你必要支持删除操纵,可能必要考虑其他的数据结构,好比计数布隆过滤器(Counting Bloom Filter)或者可逆/可更新布隆过滤器的变种,它们能够在肯定程度上支持删除,但仍旧存在肯定的限制和复杂性,而且误报率可能会随着删除操纵增加而提高。
对于RBloomFilter这个具体的类名,假如它是一个自界说实现而且确实提供了删除功能,则必要查阅该类库的具体文档或源代码以相识其如何处理删除操纵。但在大多数标准布隆过滤器场景下,删除操纵是不实用的。
3.6 扩容
布隆过滤器(Bloom Filter)在设计时,其容量是固定的,因此不支持直接扩容。传统的布隆过滤器一旦创建,它的位数组巨细就无法改变,这意味着假如必要处理的数据量凌驾了初始化时预设的容量,将导致误报率增加,且无法通过简单地增大位数组来解决这个题目。
不过,在实际应用中,为了应对数据增长的需求,可以接纳以下计谋来举行扩容:
- 并行布隆过滤器:
- 可以维护多个独立的布隆过滤器,随着数据增长,当一个过滤器填满后,新加入的数据放入新的布隆过滤器中。
- 查询时,必要对所有布隆过滤器举行查询,只有当所有的过滤器都表明元素可能不存在时,才能确定元素肯定不在集合中。
- 可扩展布隆过滤器:
- 一些变种如 Scalable Bloom Filter 或 Dynamic Bloom Filter 答应添加额外的空间,并重新哈希已有数据到更大的位数组中,从而维持较低的误报率。
- 扩容过程通常涉及构造一个新的更大容量的布隆过滤器,然后迁移旧数据到新过滤器,并从这一刻起在新过滤器中插入新数据。
- 条理结构布隆过滤器:
- 创建一个多层的布隆过滤器结构,新数据首先被插入到最顶层(最小容量)的过滤器中,当某个层级的过滤器接近饱和时,再启用下一个容量更大的过滤器。
无论接纳哪种方式扩容,都必要权衡空间效率、查询性能和错误率之间的关系,并考虑如何处理现有数据的迁移题目。同时,扩容操纵往往意味着更高的复杂性和肯定的资本开销。
4、SpringBoot 整合 布隆过滤器
4.1 技术选型
- JDK 17
- SpringBoot 3.0.0
- Redis 3.2.100
4.2 依赖
- <!--spring-boot-web-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- <version>3.0.0</version>
- </dependency>
- <!--lombok-->
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <version>1.18.26</version>
- <optional>true</optional>
- </dependency>
- <!--spring-boot-test-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <version>3.0.0</version>
- <scope>test</scope>
- </dependency>
- <!-- redis-starter -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- <version>3.0.0</version>
- </dependency>
- <!-- redis -->
- <dependency>
- <groupId>org.springframework.data</groupId>
- <artifactId>spring-data-redis</artifactId>
- <version>3.0.0</version>
- </dependency>
- <!-- 引入Redisson的Spring Boot启动器 -->
- <dependency>
- <groupId>org.redisson</groupId>
- <artifactId>redisson-spring-boot-starter</artifactId>
- <version>3.21.3</version>
- </dependency>
复制代码 4.3 配置布隆过滤器相关参数
- bloomFilter:
- # 是否启用布隆过滤器
- bloomFilterFlag: true
- # 布隆过滤器的初始大小
- MIN_EXPECTED_INSERTIONS: 8
- # 布隆过滤器的错误率
- bloomFilterErrorRate: 0.01
- # 布隆过滤器的最大使用率
- maximumUtilization: 0.90
- # 布隆过滤器的最小使用率
- minimumUtilization: 0.40
- # 布隆过滤器的初始序列号
- RBloomFilterSequence: 1
复制代码 4.4 布隆过滤器工具类
界说了一个名为BloomFilterUtil的Java类,该类用于管理布隆过滤器。类中使用了注解、依赖注入和定时任务等特性。类中的方法包括初始化本地缓存的布隆过滤器、初始化基于Redis的布隆过滤器、获取布隆过滤器、定时动态扩容布隆过滤器等。
- import com.example.demo.config.redis.RedisKeyEnum;
- import com.google.common.hash.BloomFilter;
- import com.google.common.hash.Funnels;
- import jakarta.annotation.PostConstruct;
- import jakarta.annotation.Resource;
- import lombok.extern.slf4j.Slf4j;
- import org.redisson.api.RBloomFilter;
- import org.redisson.api.RedissonClient;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.scheduling.annotation.Scheduled;
- import org.springframework.stereotype.Component;
- import java.nio.charset.Charset;
- import java.util.Arrays;
- import java.util.List;
- /**
- * BloomFilterConfig : 布隆过滤器配置文件
- *
- * @author zyw
- * @create 2024-01-10 14:43
- */
- @Slf4j
- @Component
- public class BloomFilterUtil {
- // 布隆过滤器的初始大小
- @Value("${bloomFilter.MIN_EXPECTED_INSERTIONS}")
- private int MIN_EXPECTED_INSERTIONS;
- //预期插入数据量
- private int EXPECTED_INSERTIONS;
- //布隆过滤器的错误率
- @Value("${bloomFilter.bloomFilterErrorRate}")
- private double FPP;
- //布隆过滤器的最大使用率
- @Value("${bloomFilter.maximumUtilization}")
- private double maximumUtilization;
- //布隆过滤器的最小使用率
- @Value("${bloomFilter.minimumUtilization}")
- private double minimumUtilization;
- // 布隆过滤器的初始序列号
- @Value("${bloomFilter.RBloomFilterSequence}")
- public int RBloomFilterSequence;
- // 布隆过滤器的容量自适应定时任务频率
- private static final String CRON_EXPANSION = "0 0/5 * * * ?";
- public BloomFilter<String> bloomFilter;
- public RBloomFilter<String> rBloomFilter;
- @Resource
- private RedissonClient redissonClient;
- /**
- * 初始化基于JVM本地缓存构建布隆过滤器
- */
- @PostConstruct
- public void builBloomFilter() {
- EXPECTED_INSERTIONS = MIN_EXPECTED_INSERTIONS;
- // 创建并返回BloomFilter对象
- bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), EXPECTED_INSERTIONS, FPP);
- }
- /**
- * 初始化基于Redis防止数据库查询的布隆过滤器
- *
- * @return RBloomFilter<String> 布隆过滤器
- */
- @PostConstruct
- public void buidUserRegisterCachePenetrationBloomFilter() {
- EXPECTED_INSERTIONS = MIN_EXPECTED_INSERTIONS;
- RBloomFilter<String> cachePenetrationBloomFilter = getRBloomFilter();
- cachePenetrationBloomFilter.tryInit(EXPECTED_INSERTIONS, FPP);
- initRBloomFilter(cachePenetrationBloomFilter);
- rBloomFilter = cachePenetrationBloomFilter;
- }
- /**
- * 布隆过滤器初始化
- */
- private void initRBloomFilter(RBloomFilter<String> cachePenetrationBloomFilter) {
- List<String> names = Arrays.asList("黄袁哲", "杨清涵", "陈国宇", "李康", "舒适玉", "王昊", "王浩然");
- names.parallelStream().forEach(cachePenetrationBloomFilter::add);
- }
- /**
- * 获取布隆过滤器
- */
- public RBloomFilter<String> getRBloomFilter() {
- try {
- RBloomFilter<String> bloomFilter;
- //布隆过滤器序号判断
- if (RBloomFilterSequence == 1) {
- bloomFilter = redissonClient.getBloomFilter(RedisKeyEnum.USER_REGISTER_CACHE_PENETRATION_BLOOM_FILTER_1.getKey());
- } else {
- bloomFilter = redissonClient.getBloomFilter(RedisKeyEnum.USER_REGISTER_CACHE_PENETRATION_BLOOM_FILTER_2.getKey());
- }
- if (bloomFilter == null) {
- throw new IllegalStateException("布隆过滤器不存在于Redis中");
- }
- return bloomFilter;
- } catch (Exception e) {
- log.error("从Redis获取布隆过滤器失败", e);
- throw new IllegalStateException("从Redis获取布隆过滤器失败", e);
- }
- }
- /**
- * 定时动态扩容任务配置
- */
- @Scheduled(cron = CRON_EXPANSION)
- public void dilatation() {
- // 获取当前布隆过滤器实例
- RBloomFilter<String> cachePenetrationBloomFilter = getRBloomFilter();
- // 计算当前装载因子
- long count = cachePenetrationBloomFilter.count();
- double loadFactor = (double) count / EXPECTED_INSERTIONS;
- // 检查是否需要扩容
- if (loadFactor > maximumUtilization) {
- log.info("布隆过滤器开始进行扩容", "插入元素数", count, "期望插入元素数", EXPECTED_INSERTIONS);
- // 将期望插入元素数翻倍
- EXPECTED_INSERTIONS *= 2;
- try {
- // 更新布隆过滤器序列号
- RBloomFilterSequence = RBloomFilterSequence == 1 ? 2 : 1;
- //获取新布隆过滤器实例
- RBloomFilter<String> newRBloomFilter = getRBloomFilter();
- // 尝试使用新的期望插入元素数初始化布隆过滤器
- newRBloomFilter.tryInit(EXPECTED_INSERTIONS, FPP);
- //数据初始化
- initRBloomFilter(newRBloomFilter);
- //切换成新布隆过滤器
- rBloomFilter = newRBloomFilter;
- //清除旧的缓存数据
- cachePenetrationBloomFilter.delete();
- log.info("当前布隆过滤器序号:" + RBloomFilterSequence);
- } catch (Exception e) {
- log.error("布隆过滤器初始化过程中出现异常", e);
- }
- log.info("布隆过滤器完成扩容,新容量为:" + EXPECTED_INSERTIONS);
- } else {
- log.info("当前布隆过滤器未达到扩容/缩容条件");
- }
- }
- }
复制代码 4.5 业务操纵
4.5.1 基于JVM本地缓存的 BloomFilter
- import com.example.demo.config.filter.BloomFilterUtil;
- import jakarta.annotation.Resource;
- import org.springframework.stereotype.Service;
- /**
- * BloomFilter :
- *
- * @author zyw
- * @create 2024-01-10 14:47
- */
- @Service
- public class BloomFilterService {
- /**
- * 私有的不可变的BloomFilter对象
- */
- @Resource
- private BloomFilterUtil bloomFilterUtil;
- /**
- * 向BloomFilter中添加元素
- * @param element
- */
- public void add(String element) {
- bloomFilterUtil.bloomFilter.put(element);
- }
- /**
- * 检查BloomFilter中是否存在元素
- * @param element
- * @return
- */
- public String check(String element) {
- // 如果BloomFilter判断元素可能存在
- if (bloomFilterUtil.bloomFilter.mightContain(element)) {
- return element + " 可能存在于集合中";
- } else {
- return element + " 不可能存在于集合中";
- }
- }
- }
复制代码 4.5.2 基于Redis的 RBloomFilter
- import com.example.demo.config.filter.BloomFilterUtil;
- import jakarta.annotation.Resource;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.stereotype.Service;
- /**
- * RBloomFilterService : 基于Redis的布隆过滤器服务
- *
- * @author zyw
- * @create 2024-01-10 15:38
- */
- @Service
- @Slf4j
- public class RBloomFilterService {
- @Resource
- private BloomFilterUtil bloomFilterUtil;
- /**
- * 新增数据到布隆过滤器中
- *
- * @param element
- */
- public String add(String element) {
- return bloomFilterUtil.rBloomFilter.add(element) ? "插入成功" : "插入失败";
- }
- /**
- * 检查数据是否存在布隆过滤器中
- *
- * @param element
- * @return
- */
- public String check(String element) {
- log.info("序号:{}", bloomFilterUtil.RBloomFilterSequence);
- log.info("元素个数:{}", bloomFilterUtil.rBloomFilter.count());
- log.info("期望插入数:{}", bloomFilterUtil.rBloomFilter.getExpectedInsertions());
- log.info("假阳性概率:{}", bloomFilterUtil.rBloomFilter.getFalseProbability());
- return bloomFilterUtil.rBloomFilter.contains(element) ? "存在" : "不存在";
- }
- }
复制代码 4.6 布隆过滤器测试控制层
- import com.example.demo.config.filter.BloomFilterUtil;
- import com.example.demo.filter.BloomFilterService;
- import com.example.demo.filter.RBloomFilterService;
- import io.swagger.v3.oas.annotations.Operation;
- import io.swagger.v3.oas.annotations.tags.Tag;
- import jakarta.annotation.Resource;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.bind.annotation.RestController;
- /**
- * BloomFilterController :
- *
- * @author zyw
- * @create 2024-01-10 14:58
- */
- @Slf4j
- @RestController
- @Tag(name = "布隆过滤器测试控制器")
- @RequestMapping("bloom")
- public class BloomFilterController {
- @Resource
- private BloomFilterService bloomFilterService;
- @Resource
- private RBloomFilterService rBloomFilterService;
- @Resource
- private BloomFilterUtil bloomFilterConfig;
- @GetMapping("/add")
- @Operation(summary = "基于JVM本地缓存布隆过滤器-添加")
- public void add(@RequestParam String element){
- bloomFilterService.add(element);
- }
- @GetMapping("/check")
- @Operation(summary = "基于JVM本地缓存布隆过滤器-检查")
- public String check(@RequestParam String element){
- return bloomFilterService.check(element);
- }
- @GetMapping("/addR")
- @Operation(summary = "基于Redis布隆过滤器-添加")
- public void addR(@RequestParam String element){
- rBloomFilterService.add(element);
- }
- @GetMapping("/checkR")
- @Operation(summary = "基于Redis布隆过滤器-检查")
- public String checkR(@RequestParam String element){
- return rBloomFilterService.check(element);
- }
- @GetMapping("/dilatationR")
- @Operation(summary = "基于Redis布隆过滤器-手动扩容")
- public void dilatationR(){
- bloomFilterConfig.dilatation();
- }
- }
复制代码 5、测试
5.1 项目启动,初始化布隆过滤器
5.2 测试数据是否存在
5.3 触发扩容
- 我们插入一条数据,使 cachePenetrationBloomFilter.count() 元素到达8个
- 我们的渴望插入数据量EXPECTED_INSERTIONS为8,此时布隆过滤器的使用率为 count / EXPECTED_INSERTIONS = 1
- 凌驾我们设定的布隆过滤器的最大使用率maximumUtilization 0.9
- 我们计算出新的渴望插入数据量EXPECTED_INSERTIONS为8*2=16,并构建,初始化
- 初始化完成后,再替换旧的布隆过滤器,随机删除之前缓存的数据。
- 以此防止数据量过大时,初始化数据执行时间过长而影响过滤器的正常使用
在Redis可视化工具中,我们可以看到扩容后的过滤器
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |