布隆过滤器解决Redis缓存穿透

448 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

背景

上一章讲解了如何解决缓存雪崩、击穿、穿透问题,针对缓存穿透的问题,我们可以采取布隆过滤器来解决,本文将讲解布隆过滤器的原理和使用。

简介

布隆过滤器一种基于概率的数据结构,主要用来判断某个元素是否在集合内,它能够告诉你某个元素一定不在集合内或可能在集合内。

应用场景

  • Redis缓存穿透问题
  • 邮件过滤,使用布隆过滤器来做邮件黑名单过滤
  • 对爬虫网址进行过滤
  • 解决新闻推荐过的不再推荐问题
  • HBase\RocksDB\LevelDB等数据库内置布隆过滤器,用于判断数据是否存在,可以减少数据库的IO请求

基本原理

布隆过滤器是一个位数组(与bitmap数据结构类似)和K个映射函数,在初始状态时,对于长度为M的位数组array,它的所有位置都被初始为0。当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点(offset)把它们置为1,检索时如果这些点有任何一个0,则被检元素一定不在,如果都是1则被检元素很可能存在。

如下图所示:

图片.png

说明:如上图中的Bloom Filter假设具有三个哈希函数,预先存入a、b、c三个元素并根据哈希函数计算出不同的bit位,将这些bit位置都为1。

  • 判断c,根据三个哈希函数计算出不同的bit位置,发现这些位置的bit值都是1,Bloom Filter返回true表示该元素存在,实际上该元素确实存在。

  • 判断d,根据三个哈希函数计算出不同的bit位置,发现函数hash3(d)结果对应的的bit索引位的值是0,Bloom Filter返回false表示该元素不存在,实际上该元素确实不存在。

  • 判断e,根据三个哈希函数计算出不同的bit位置,发现这些位置的bit值都是1,Bloom Filter返回true表示该元素存在,实际上该元素是不存在,说明此时发生了误判。

具体实现

基于Guava实现

采用Google的Guava实现的布隆过滤器(Bloom Filter)来实现。

jar包引入

 <dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>18.0</version>
</dependency>

测试示例

public class BLFilter
{
    private static Logger logger=LoggerFactory.getLogger(BLFilter.class);
    public static void main(String[] args)
    {
        // 1.创建布隆过滤器,预期数据量10000,错误率0.0001
        BloomFilter<CharSequence> bloomFilter =
                BloomFilter.create(Funnels.stringFunnel(
                        Charset.forName("utf-8")),10000, 0.0001);
        
        // 2.添加数据
        for (int i = 0; i < 5000; i++) 
        {
            bloomFilter.put("" + i);
        }
        
        logger.info("数据插入完毕");
        
        // 3.测试结果输出
        for (int i = 0; i < 10000; i++) 
        {
            if (bloomFilter.mightContain(String.valueOf(i))) 
            {
                logger.info(i + "存在");
            }
            else 
            {
                logger.info(i + "不存在");
            }
        }
    }
}

说明:

基于Redisson实现

引入jar包

  <dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson-spring-boot-starter</artifactId>
   <version>3.16.7</version>
</dependency>

具体实现

@Service
public class BloomFilterService
{
    @Autowired
    private RedissonClient redissonClient;
    
    public <T> RBloomFilter<T> create(String filterName, long expectedInsertions, double falseProbability) {
        RBloomFilter<T> bloomFilter = redissonClient.getBloomFilter(filterName);
        bloomFilter.tryInit(expectedInsertions, falseProbability);
        return bloomFilter;
    }
}

说明:如果是Redis Cluster的集群环境,则需要采用如下方式:

`RClusteredBloomFilter<SomeObject> bloomFilter = redisson.getClusteredBloomFilter("sample");`

测试代码

    @RequestMapping("/bloomFilterTest")
    public void bloomFilterTest()
    {
        // 预期插入数量
        long expectedInsertions = 10000L;
        // 错误比率
        double falseProbability = 0.01;
        RBloomFilter<Object> bloomFilter = bloomFilterService.create("ipBlackList", expectedInsertions, falseProbability);
        // 布隆过滤器增加元素
        for (long i = 0; i < expectedInsertions; i++) 
        {
            bloomFilter.add("js"+i);
        }
        
        //用来统计误判的个数
        int count = 0;
        //查询不存在的数据一千次
        for (int i = 0; i < 1000; i++) {
            if (bloomFilter.contains("zhangsan" + i)) {
                count++;
            }
        }
        
        logger.info("判断错误的个数:"+count);
        logger.info("js1是否在过滤器中存在:"+bloomFilter.contains("js1"));
        logger.info("js222是否在过滤器中存在:"+bloomFilter.contains("js222"));
        logger.info("预计插入数量:" + bloomFilter.getExpectedInsertions());
        logger.info("容错率:" + bloomFilter.getFalseProbability());
        logger.info("hash函数的个数:" + bloomFilter.getHashIterations());
        logger.info("插入对象的个数:" + bloomFilter.count());
    }

输出结果

图片.png

优缺点

优点

  • 时间复杂度低,增加和查询元素的时间复杂为O(N)
  • 保密性强,布隆过滤器不存储元素本身
  • 存储空间小,非常节省空间的

缺点

  • 存在一定的误判率,可以通过调整参数来降低。
  • 无法获取元素本身
  • 删除元素困难,因为删除会把相应的k个bits位置为0,而其中很有可能有其他元素执行哈希计算之后也会对应该bit位,从而造成更多的误判.

总结

本文详细讲解了布隆过滤器的原理和实现,如有疑问请随时提问。