布隆过滤器--redis缓存穿透最佳解决方案

219 阅读5分钟

1. 布隆过滤器是什么

布隆过滤器是一个精巧而且经典的数据结构,使用布隆过滤器可以兼顾空间效率时间效率来判断一个数是否存在。

2. 为什么要使用布隆过滤器

布隆过滤器最常见的应用就是解决缓存穿透的问题

缓存穿透

缓存穿透就是请求接口的资源在redis中不存在,然后查询数据库发现也不存在,从而一直不能更新缓存,导致所有的请求都访问到数据库,但由于数据库的读写效率较低,在并发量大的情况下就会导致数据库宕机。

image-20230416140526706

通常我们可以向分布式缓存中写入一个过期时间较短的空值占位,但这样会占用较多的存储空间,而使用布隆过滤器就可以兼顾空间和效率来判断一个数据是否存在。

3. 布隆过滤器的原理

布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。当一个数据插入时会使用K个哈希函数,生成数据对应的K个hash值,并将K个hash值作为数组下标,把数组对应位置的值修改为1,在后续判断时就判断数据K个hash值对应数组下标的值是不是1,若存在至少一个不为1就判断数据不存在。

布隆过滤器可能会存在误判,比如一个不存在的数据k个hash值的位置都因为其他的数据的插入而变成1了,布隆过滤器会误判该数据存在,所以布隆过滤器判断存在只是可能存在,但判断不存在就一定不存在

image-20230416141817375

比如有3个hash函数

x的3个哈希值分别是0,4,7,则将对应位置改为1

y的3个哈希值分别是1,4,6,则将对应位置改为1

可以看到对于下标4对应的位置被两个数据(x,y)共用了

布隆过滤器包含如下四个属性

  • k : 哈希函数个数
  • m : 位数组长度
  • n : 插入的元素个数
  • p : 误判率

哈希函数的个数越多,检索的速度会越慢,越少误判率也越高

位数组长度太大,空间效率较低,太小误判率又太高

所以我们如何确定合适的位数组和哈希函数的个数呢?

我们可以通过下面的公式,根据我们需要插入的元素个数和期望的误判率计算出合适的哈希函数的个数K和位数组长度m

image-20230416142941775

布隆过滤器不支持删除

由于每一个二进制位对应的数据可能被多个数据共用,所以布隆过滤器不支持删除,因为删除一个数据(x)对应二进制位的数据可能导致其他已经存在的数据判断不存在(y)

但是实际环境中肯定存在需要删除元素的场景,那么可以通过以下两种方式解决

  1. 计数布隆过滤器

    即数组每一位的值不只是单纯的0和1代表存在和不存在,而是通过计数表示多少个存在的数的hash值使用了这一个bit位

    image-20230416150407307

    每次插入时就将对应的bit位+1,删除时将对应位置-1

    但是这种方式就不能对于每个位置使用一个bit位了,会造成大量空间的浪费

  2. 定时重新构建布隆过滤器

    1. 定时任务触发全量商品查询,根据查询结果为生成一个新的布隆过滤器
    2. 修改商品布隆过滤器的映射
    3. 客户端通过映射找到新的布隆过滤器的key值,并通过新的key值获取布隆过滤器进行判断
    4. 删除原来的过滤器

4. 布隆过滤器的开源实现

Redisson

Redisson的使用

  1. 添加Maven依赖

    <dependency>
       <groupId>org.redisson</groupId>
       <artifactId>redisson</artifactId>
       <version>3.16.1</version>
    </dependency>
    
  2. 配置 Redisson 客户端

    @Configuration
    public class RedissonConfig {
    ​
     Bean
     public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        return Redisson.create(config);
     }
     
    }
    ​
    
  3. 初始化

    RBloomFilter<Long> bloomFilter = redissonClient.
                                          getBloomFilter("myBloomFilter");
    //10000表示插入元素的个数,0.001表示误判率
    bloomFilter.tryInit(100000.001);
    //插入4个元素
    bloomFilter.add(1L);
    bloomFilter.add(2L);
    bloomFilter.add(3L);
    bloomFilter.add(4L);
    
  4. 判断数据是否存在

    public boolean mightcontain(Long id) {
        return bloomFilter.contains(id);
    }
    

Redisson的原理

  1. 初始化的时候,会创建一个 Hash 数据结构的 key ,计算并存储布隆过滤器的4个核心属性

    image-20230416143611095

  2. Redisson 布隆过滤器操作的对象是 位图(bitMap) ,位图底层使用的是String数据结构,可以存储512M长度的字符,每个字符又是8位,所以可以存储长达2^ 32次方个bit位,使用getbit/setbit命令来处理这个位数组

  3. 添加元素时就通过计算hash函数对应的bit位,然后使用setbit来将对应位置设置为1就行了

  4. 判断时就通过getbit来判断是否存在不为1的位置

5. 布隆过滤器解决缓存雪崩问题

在访问数据前先通过布隆过滤器判断数据是否存在,若不存在则直接返回不存在,就避免了无法更新缓存,大量并发访问数据库的情况了

image-20230416145822738

6. 总结

  1. 布隆过滤器是一个很长的二进制向量和一系列随机映射函数,用于检索一个元素是否存在空间效率查询时间远远超过一般的算法,但是有一定的误判率
  1. 布隆过滤器的四个核心属性:
  • k : 哈希函数个数
  • m : 位数组长度
  • n : 插入的元素个数
  • p : 误判率
  1. 可以通过计数布隆过滤器定时重新构建布隆过滤器两种方案实现删除元素的效果。

参考

[聊聊布隆过滤器] juejin.cn/post/722215…