【业务场景实战】你知道布隆过滤器怎么用吗?

布隆过滤器想必大家都听过,背过Redis面试题的兄弟应该都知道,布隆过滤器是解决缓存穿透问题的一种方法。但可能很少用过

布隆过滤器主要是为了解决海量数据的存在性问题。对于海量数据中判定某个数据是否存在且容忍轻微误差这一场景(比如缓存穿透、海量数据去重)来说,非常适合。

一、简介

布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的 List、Map、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除

布隆过滤:布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中。

假设布隆过滤器判断这个数据不存在,则直接返回

简单来说,布隆过滤器就是一个集合,用来存放数据,同时用以判断数据是否存在于这个集合中。这种方式优点在于节约内存空间,但存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突。

正常来说,数据量越大,出现误判的可能性就越大。

二、使用场景

  1. 判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,上亿)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤(判断一个邮件地址是否在垃圾邮件列表中)、黑名单功能(判断一个 IP 地址或手机号码是否在黑名单中)等等。

  2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重、对巨量的 QQ 号/订单号去重。

去重场景也需要用到判断给定数据是否存在,因此布隆过滤器主要是为了解决海量数据的存在性问题。

下面我们来简单使用一下

三、简单使用

1、导入依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.0-jre</version>
</dependency>

2、代码测试

// 创建布隆过滤器对象:
// 参数1 = 预计插入的元素数量, 参数2 = 容错率(期望的误判率)
BloomFilter<Integer> filter = BloomFilter.create(
Funnels.integerFunnel(),
1500,
0.01);
// 判断指定元素是否存在
System.out.println(filter.mightContain(1));//false
System.out.println(filter.mightContain(2));//false
// 将元素添加进布隆过滤器
filter.put(1);
filter.put(2);
System.out.println(filter.mightContain(1));//true
System.out.println(filter.mightContain(2));//true

四、项目实战

下面我们用布隆过滤器来解决缓存穿透

虽然缓存穿透问题可以用布隆过滤器来解决,但是布隆过滤器中的数据不易被删除,如果数据库中的数据有新增或者删除,布隆过滤器无法及时删除或新增数据。

上面所介绍的布隆过滤器其实有个问题

它最大的一个问题是它只能在同一个方法里面进行判断,不同的线程所创建的布隆过滤器会不一样。

比如说你在一个方法里面创建好了一个布隆过滤器,也添加了一些元素;但如果你在另一个方法里面去判断是查询不到的,也就类似于本地和分布式的区别。

所以我们这里采用Redis布隆过滤器。

Redis布隆过滤器需要在Redis上集成一个插件,选择对应Redis版本的插件,我这里使用的是Redis6,具体下载流程看:下载流程:硬核 | Redis 布隆(Bloom Filter)过滤器原理与实战-腾讯云开发者社区-腾讯云 (tencent.com)

1、缓存穿透思路

在数据到达数据库时,应该先判断布隆过滤器中是否存在key值,但是有个问题

布隆过滤器中原本不存在数据,那么又如何去判断数据是否存在?

我们需要先将数据添加到过滤器中,然后再让其发挥作用

布隆过滤器中数据Key的对象应该对应数据库,而不是缓存。

代码逻辑:

  • 首先判断请求值key是否存在于布隆过滤器中

  • 不存在,直接返回

  • 存在,查询缓存中是否有对应数据

    • 有,直接返回数据

    • 没有,查询数据库

      • 不存在,返回空数据

      • 存在,更新缓存,返回数据

key过了布隆过滤器表示数据库中存在id对应的数据,剩下两种:

  • 缓存key不存在—查询数据库

  • 缓存key存在

    1. key为空值

    2. key中数据存在,直接返回

所以我们这里需要过滤掉key中数据为空的情况,保证放回的是真实存在的数据.

2、代码实现

1)导入依赖
<dependency>
             <groupId>org.redisson</groupId>
             <artifactId>redisson</artifactId>
             <version>3.21.3</version>
         </dependency>
2)配置Redisson

注意:如果项目中用到了Redis,数据库要和它的区别开,不然运行会发生冲突的

# 分布式限流Redisson 配置
  redisson:
    database: 1
    host: 
    port: 6379
    timeout: 5000
    password: 
3)Redisson配置类
package com.example.config;

import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 分布式限流
 */
@Configuration
@ConfigurationProperties(prefix = "spring.redisson")
@Data
public class RedissonConfig {
    private String host;
    private Integer database;
    private Integer port;
    private String password;

    //spring启动时,自动创建RedissonClient对象
    @Bean
    public RedissonClient getRedissonClient() {
        Config config = new Config();
        // 设置使用单个服务器
        config.useSingleServer()
                // 设置数据库
                .setDatabase(database)
                // 设置地址
                .setAddress("redis://" + host + ":" + port)
                // 设置密码
                .setPassword(password);
        // 创建Redisson客户端
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}
2)布隆过滤管理类

创建布隆过滤器管理类,设置插入的元素数量和容错率

一般来说,使用管理类是为了降低代码的耦合性,可以让代码看上去更加美观,当然最重要的是实用。管理类不依赖于项目中的逻辑,在任何项目中都可以写入复用。


@Service
 public class BloomFilterManager {
 ​
     @Resource
     private RedissonClient redissonClient;
     /**
      * 创建布隆过滤器
      *
      * @param filterName         过滤器名称
      * @param expectedInsertions 预测插入数量
      * @param falsePositiveRate  误判率
      */
     public <T> RBloomFilter<T> create(String filterName, long expectedInsertions, double falsePositiveRate) {
         RBloomFilter<T> bloomFilter = redissonClient.getBloomFilter(filterName);
         bloomFilter.tryInit(expectedInsertions, falsePositiveRate);
         return bloomFilter;
     }
 }
3)查询数据ID
@Select("SELECT id FROM tb_shop")
     List<Long> selectAllIds();

在shopMapper中添加这段代码,查询数据库中所有数据的ID

4)布隆过滤器初始化

这步不知道你们能不能想到,有没有觉得很熟悉,嗯?

对了,上面一篇文章我讲过的,初始化注解,代码都还没删呢

项目启动时,初始化创建布隆过滤器,并把查询到的ID都插入过滤器中

通过Redis布隆过滤器,就相当于在Redis中创造了一个特殊容器来存储数据

这段代码的逻辑就是定义一个布隆过滤器,然后把查询到的shopid都写入到Redis布隆过滤器中

然后就可以在实际方法里面去查询指定的shopId元素是否真的存在了

@PostConstruct
 public void init() {
     //myMessageProducer.sendMessage("hmdp_exchange", "hmdp_routingKey", String.valueOf(1L));
     // 启动项目时初始化bloomFilter
     bloomFilter = bloomFilterManager.create(BLOOM_FILTER_SHOP, expectedInsertions, falseProbability);
     // 设置缓存时间
     int timeToLive = new Random().nextInt(200) + 300;
     // 设置bloomFilter的过期时间
     redissonClient.getBucket(BLOOM_FILTER_SHOP).set(null, timeToLive, TimeUnit.SECONDS);
     List<Long> list = shopMapper.selectAllIds();
     for (Long shopId : list) {
         bloomFilter.add(shopId);
     }
     long elementCount = bloomFilter.count();
     log.info("布隆过滤器加入元素个数为:{}.", elementCount);
 }

初始化后,启动项目后,控制台也会自动执行查询方法,初始化成功

方法一: 启动项目时初始化bloomFilter,将数据库中查询的ID都添加到过滤器中

方法二:

  • 首先判断请求值key是否存在于布隆过滤器中

  • 不存在,直接返回

  • 存在,查询缓存中是否有对应的数据

    • 有,直接返回数据

    • 没有,查询数据库

      • 不存在,返回空

      • 存在,更新缓存,返回数据

接下来我们需要来模拟情况:

1、数据库中存在,缓存中不存在

预期:能通过布隆,最后到达数据库,并将查询到的数据存入Redis

2、数据库和缓存中数据均存在

预期:通过布隆,到达缓存直接返回

3、布隆中key不存在

预期:直接失败

4、布隆中key存在,缓存中key存在但为空,数据库中数据不存在

代码小抄

最后来看结果:

今天的讲解就到这里,期待下期再见!希望的话,欢迎点赞关注!

  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
布隆过滤器是一个非常实用的数据结构,可以用于快速判断一个元素是否在一个集合中。在实际应用中,布隆过滤器经常被用来作为缓存、数据去重、黑名单过滤等场景中。 下面我们以 Python 为例,来实现一个简单的布隆过滤器。 首先,我们需要选取几个合适的哈希函数,用于将元素映射成一些位上的位置。在这里,我们使用了 MurmurHash 和 FNV 哈希函数。 ```python import mmh3 import fnv class BloomFilter: def __init__(self, capacity, error_rate): self.capacity = capacity self.error_rate = error_rate self.num_bits = self.get_num_bits(capacity, error_rate) self.num_hashes = self.get_num_hashes(self.num_bits, capacity) self.bit_array = [False] * self.num_bits def add(self, element): for seed in range(self.num_hashes): index = self.get_hash(element, seed) % self.num_bits self.bit_array[index] = True def contains(self, element): for seed in range(self.num_hashes): index = self.get_hash(element, seed) % self.num_bits if not self.bit_array[index]: return False return True def get_num_bits(self, capacity, error_rate): return int(-capacity * math.log(error_rate) / (math.log(2) ** 2)) def get_num_hashes(self, num_bits, capacity): return int(num_bits / capacity * math.log(2)) def get_hash(self, element, seed): return mmh3.hash(element, seed) ^ fnv.hash(str(seed) + element) ``` 在代码中,我们定义了一个 BloomFilter 类,它包含了以下几个方法: 1. `__init__` 方法:用于初始化布隆过滤器,传入参数为期望存储元素的数量和错误率。 2. `add` 方法:用于将元素添加到布隆过滤器中。 3. `contains` 方法:用于查询元素是否存在于布隆过滤器中。 4. `get_num_bits` 方法:用于计算布隆过滤器需要使用的位数。 5. `get_num_hashes` 方法:用于计算布隆过滤器需要使用的哈希函数数量。 6. `get_hash` 方法:用于计算元素被哈希后的值。 接下来我们可以使用上述代码来实现一个简单的例子: ```python # 初始化布隆过滤器 bloom_filter = BloomFilter(1000, 0.01) # 添加元素 bloom_filter.add("hello") bloom_filter.add("world") # 查询元素 print(bloom_filter.contains("hello")) # True print(bloom_filter.contains("world")) # True print(bloom_filter.contains("python")) # False ``` 在这个例子中,我们首先初始化了一个容量为 1000,误判率为 0.01 的布隆过滤器。之后我们添加了 "hello" 和 "world" 两个元素,并查询了这两个元素是否存在于布隆过滤器中。最后我们查询了一个不在布隆过滤器中的元素 "python",它返回了 False。 需要注意的是,布隆过滤器的误判率是可以通过调整哈希函数的数量和位数来进行控制的。一般来说,哈希函数数量越多、位数越大,误判率越小,但是占用的内存空间也会越大。 这就是一个简单的布隆过滤器实现,希望能对你有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值