redis中布隆过滤器使用详解

Posted 斗者_2013

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis中布隆过滤器使用详解相关的知识,希望对你有一定的参考价值。

文章目录


一、布隆过滤器介绍

1、什么是布隆过滤器

布隆过滤器(英语:Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。主要用于判断一个元素是否在一个集合中

通常我们会遇到很多要判断一个元素是否在某个集合中的业务场景,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间也会呈现线性增长,最终达到瓶颈。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为O(n),O(logN),O(1)

这个时候,布隆过滤器(Bloom Filter)就应运而生。

2、布隆过滤器实现原理

如果想判断一个元素是不是在一个集合中,一般想到的方法是暂存数据,然后查找判定是否存在集合中。这种方法在数据量比较小的情况下适用,但是几个中元素较多的时候,检索速度就会越来越慢。

可以利用Bitmap:只要检查对应点是不是1就可以知道集合中有没有这个数。Bloom filter可以看做是对bitmap的扩展,只是使用多个hash映射函数,从而减低hash发生冲突的概率。

算法如下:

BloomFilter 是由一个固定大小的二进制向量或者位图(bitmap)和一系列映射函数组成的。

  1. 在初始状态时,对于长度为 m 的位数组,它的所有位都被置为0;
  2. 当有变量被加入集合时,通过 K 个映射函数将这个变量映射成位图中的 K 个点,把它们置为 1;
  3. 查询某个变量是否存在的时候我们只要看看这些点是不是都是 1 就可以大概率知道集合中有没有它了。
  • 如果这些点有任何一个 0,则被查询变量一定不在;
  • 如果都是 1,则被查询变量很可能存在
    为什么说是可能存在,而不是一定存在呢?那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。

3、误判率

这里的误判率是指,BloomFilter 判断某个 key 存在,但它实际不存在的概率

布隆过滤器的误判是由于多个输入经过哈希之后在相同的bit位 置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射且置 1。

散列函数本身就是会出现碰撞,尽管布隆过滤器中会采用多重hash计算降低冲突概率,但还是无法完全避免,这样就导致一个对象经过多重hash计算的bit位可能和其他对象的bit位重合,如果一个新对象的bit位都被存入其他对象时置为1,那么就会出现误判。

这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素。

布隆过滤器误判图解:
1、初始状态:布隆过滤器中的bit数组都为0;
2、向布隆过滤器中添加对象X和对象Y,分别经过多重hash计算后,将bit数组中的1,2,4,5,7位置填充为1;
3、当用布隆过滤器判断对象Z是否存在时,先对对象Z进行多重hash计算,对应bit位为4,5,7,而4,5,7刚好已经被对象X和对象Y填充为1,这时就会误判对象Z已经存在。
4、极限情况下当bit数组中的bit位全部被置为1,那么布隆过滤器将会失效。

特性:

  • 一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。
  • 布隆过滤器可以添加元素,但是不能删除元素,因为删掉元素会导致误判率增加。

怎么降低误判率?

  1. 增加散列函数个数,减少bit位冲突的可能;
  2. 增加Bitmap的大小,避免bit位大量覆盖填充。

4、布隆过滤器使用场景

布隆过滤器的典型应用有:

  • 数据库防止穿库, Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter来减少不存在的行或列的磁盘查找。避免代价高昂的磁盘查找会大大提高数据库查询操作的性能。
  • 业务场景中判断用户是否阅读过某视频或文章,比如抖音或头条,当然会导致一定的误判,但不会让用户看到重复的内容。
  • 缓存宕机、缓存击穿场景,一般判断用户是否在缓存中,如果在则直接返回结果,不在则查询db,如果来一波冷数据,会导致缓存大量击穿,造成雪崩效应,这时候可以用布隆过滤器当缓存的索引,只有在布隆过滤器中,才去查询缓存,如果没查询到,则穿透到db。如果不在布隆器中,则直接返回。
  • WEB拦截器,如果相同请求则拦截,防止重复被攻击。用户第一次请求,将请求参数放入布隆过滤器中,当第二次请求时,先判断请求参数是否被布隆过滤器命中。可以提高缓存命中率。
  • Squid 网页代理缓存服务器在 cache digests 中就使用了布隆过滤器。Google Chrome浏览器使用了布隆过滤器加速安全浏览服务

总的来说,布隆过滤器是用于大数据场景下的重复判断,并且允许有一定误差存在,最典型的使用是解决缓存穿透问题。

5、哈希表与布隆过滤器比较

哈希表也能用于判断元素是否在集合中,但是Bloom Filter只需要哈希表的1/8或1/4的空间复杂度就能完成同样的问题。
哈希表是将真实的元素存放到集合中,而Bloom Filter只是根据元素的多重hash计算结果填充二进制数组,并不存放真正的对象。

Bloom Filter可以插入元素,但是不可以删除已有元素。集合中的元素越多,误报率越大,但是不会漏报。

二、redis中布隆过滤器实战

“纸上谈来终觉浅,绝知此事要躬行”,接下来让我们实战下如何通过布隆过滤器避免订单信息查询的缓存穿透问题。

1.引入redisson依赖

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

2.创建订单表

CREATE TABLE `tb_order` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单Id',
  `order_desc` varchar(50) NOT NULL COMMENT '订单描述',
  `user_id` bigint NOT NULL COMMENT '用户Id',
  `product_id` bigint NOT NULL COMMENT '商品Id',
  `product_num` int NOT NULL COMMENT '商品数量',
  `total_account` decimal(10,2) NOT NULL COMMENT '订单金额',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `ik_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

3.配置redis

新增redis连接属性:

spring.redis.host=192.168.206.129
spring.redis.port=6379
spring.redis.password=123456

配置redisTemplate,主要是设置序列化策略Jackson

@Configuration
public class RedisConfig 

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedissonConnectionFactory redisConnectionFactory) 
        //设置序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //反序列化,该设置不能省略,不然从redis获取json转为实体时会报错
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.WRAPPER_ARRAY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        //配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        RedisSerializer stringSerializer = new StringRedisSerializer();
        //key序列化
        redisTemplate.setKeySerializer(stringSerializer);
        //value序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    


4.配置BloomFilter

/**
 * 配置布隆过滤器
 */
@Configuration
public class BloomFilterConfig 
    @Autowired
    private RedissonClient redissonClient;
    /**
     * 创建订单号布隆过滤器
     * @return
     */
    @Bean
    public RBloomFilter<Long> orderBloomFilter() 
        //过滤器名称
        String filterName = "orderBloomFilter";
        // 预期插入数量
        long expectedInsertions = 10000L;
        // 错误比率
        double falseProbability = 0.01;
        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(filterName);
        bloomFilter.tryInit(expectedInsertions, falseProbability);
        return bloomFilter;
    

5.创建订单

redisson中的BloomFilter有2个核心方法:

  • bloomFilter.add(orderId) 向布隆过滤器中添加id
  • bloomFilter.contains(orderId) 判断id是否存在
@Slf4j
@Service
public class OrderServiceImpl implements OrderService 

    @Resource
    private RBloomFilter<Long> orderBloomFilter;

    @Resource
    private TbOrderMapper  tbOrderMapper;

    @Resource
    private RedisTemplate<String,Object> redisTemplate;


    @Override
    public void createOrder(TbOrder tbOrder) 
        //1、创建订单
        tbOrderMapper.insert(tbOrder);

        //2、订单id保存到布隆过滤器
        log.info("布隆过滤器中添加订单号:",tbOrder.getId());
        orderBloomFilter.add(tbOrder.getId());
    

    @Override
    public TbOrder get(Long orderId) 
        TbOrder tbOrder = null;
        //1、根据布隆过滤器判断订单号是否存在
        if(orderBloomFilter.contains(orderId))
            log.info("布隆过滤器判断订单号存在",orderId);
            String key = "order:"+orderId;
            //2、先查询缓存
            Object object = redisTemplate.opsForValue().get(key);
            if(object != null)
                log.info("命中缓存");
                tbOrder =  (TbOrder)object;
            else
                //3、缓存不存在则查询数据库
                log.info("未命中缓存,查询数据库");
                tbOrder = tbOrderMapper.selectById(orderId);
                redisTemplate.opsForValue().set(key,tbOrder);
            
        else
            log.info("判定订单号不存在,不进行查询",orderId);
        
        return tbOrder;
    

6.单元测试

    @Test
    public void testCreateOrder() 
        for (int i = 0; i < 50; i++) 
            TbOrder tbOrder = new TbOrder();
            tbOrder.setOrderDesc("测试订单"+(i+1));
            tbOrder.setUserId(1958L);
            tbOrder.setProductId(102589L);
            tbOrder.setProductNum(5);
            tbOrder.setTotalAccount(new BigDecimal("300"));
            tbOrder.setCreateTime(new Date());
            orderService.createOrder(tbOrder);
        
    

    @Test
    public void testGetOrder() 
        TbOrder  tbOrder = orderService.get(25L);
        log.info("查询结果:", tbOrder.toString());
    

总结

布隆过滤器的原理其实非常简单,就是bitmap + 多重hash,主要优势就是利用非常小的空间就可以实现在大规模数据下快速判断某一对象是否存在,缺点是存在误判的可能,但不会漏判,也就是存在的对象一定会判断为存在,而不存在的对象会有较低的概率为误判为存在,且不支持对象的删除,因为会增加误判的概率。最典型的使用是解决缓存穿透的问题。

Redis详解(十三)------ Redis布隆过滤器

原文:Redis详解(十三)------ Redis布隆过滤器

 


  本篇博客我们主要介绍如何用Redis实现布隆过滤器,但是在介绍布隆过滤器之前,我们首先介绍一下,为啥要使用布隆过滤器。

1、布隆过滤器使用场景

  比如有如下几个需求:

  ①、原本有10亿个号码,现在又来了10万个号码,要快速准确判断这10万个号码是否在10亿个号码库中?

  解决办法一:将10亿个号码存入数据库中,进行数据库查询,准确性有了,但是速度会比较慢。

  解决办法二:将10亿号码放入内存中,比如Redis缓存中,这里我们算一下占用内存大小:10亿*8字节=8GB,通过内存查询,准确性和速度都有了,但是大约8gb的内存空间,挺浪费内存空间的。

  ②、接触过爬虫的,应该有这么一个需求,需要爬虫的网站千千万万,对于一个新的网站url,我们如何判断这个url我们是否已经爬过了?

  解决办法还是上面的两种,很显然,都不太好。

  ③、同理还有垃圾邮箱的过滤。

  那么对于类似这种,大数据量集合,如何准确快速的判断某个数据是否在大数据量集合中,并且不占用内存,布隆过滤器应运而生了。

2、布隆过滤器简介

  带着上面的几个疑问,我们来看看到底什么是布隆过滤器。

  布隆过滤器:一种数据结构,是由一串很长的二进制向量组成,可以将其看成一个二进制数组。既然是二进制,那么里面存放的不是0,就是1,但是初始默认值都是0。

  如下所示:

  技术图片

  ①、添加数据

  介绍概念的时候,我们说可以将布隆过滤器看成一个容器,那么如何向布隆过滤器中添加一个数据呢?

  如下图所示:当要向布隆过滤器中添加一个元素key时,我们通过多个hash函数,算出一个值,然后将这个值所在的方格置为1。

  比如,下图hash1(key)=1,那么在第2个格子将0变为1(数组是从0开始计数的),hash2(key)=7,那么将第8个格子置位1,依次类推。

  技术图片

 

  ②、判断数据是否存在?

  知道了如何向布隆过滤器中添加一个数据,那么新来一个数据,我们如何判断其是否存在于这个布隆过滤器中呢?

  很简单,我们只需要将这个新的数据通过上面自定义的几个哈希函数,分别算出各个值,然后看其对应的地方是否都是1,如果存在一个不是1的情况,那么我们可以说,该新数据一定不存在于这个布隆过滤器中。

  反过来说,如果通过哈希函数算出来的值,对应的地方都是1,那么我们能够肯定的得出:这个数据一定存在于这个布隆过滤器中吗?

  答案是否定的,因为多个不同的数据通过hash函数算出来的结果是会有重复的,所以会存在某个位置是别的数据通过hash函数置为的1。

  我们可以得到一个结论:布隆过滤器可以判断某个数据一定不存在,但是无法判断一定存在

  ③、布隆过滤器优缺点

  优点:优点很明显,二进制组成的数组,占用内存极少,并且插入和查询速度都足够快。

  缺点:随着数据的增加,误判率会增加;还有无法判断数据一定存在;另外还有一个重要缺点,无法删除数据。

3、Redis实现布隆过滤器

①、bitmaps

  我们知道计算机是以二进制位作为底层存储的基础单位,一个字节等于8位。

  比如“big”字符串是由三个字符组成的,这三个字符对应的ASCII码分为是98、105、103,对应的二进制存储如下:

  技术图片

 

 

  在Redis中,Bitmaps 提供了一套命令用来操作类似上面字符串中的每一个位。

  一、设置值

setbit key offset value

  技术图片

 

   我们知道"b"的二进制表示为0110 0010,我们将第7位(从0开始)设置为1,那0110 0011 表示的就是字符“c”,所以最后的字符 “big”变成了“cig”。

  二、获取值

gitbit key offset

  技术图片

   三、获取位图指定范围值为1的个数

bitcount key [start end]

  如果不指定,那就是获取全部值为1的个数。

  注意:start和end指定的是字节的个数,而不是位数组下标。

  技术图片

②、Redisson

  Redis 实现布隆过滤器的底层就是通过 bitmap 这种数据结构,至于如何实现,这里就不重复造轮子了,介绍业界比较好用的一个客户端工具——Redisson。

  Redisson 是用于在 Java 程序中操作 Redis 的库,利用Redisson 我们可以在程序中轻松地使用 Redis。

  下面我们就通过 Redisson 来构造布隆过滤器。

技术图片
 1 package com.ys.rediscluster.bloomfilter.redisson;
 2 
 3 import org.redisson.Redisson;
 4 import org.redisson.api.RBloomFilter;
 5 import org.redisson.api.RedissonClient;
 6 import org.redisson.config.Config;
 7 
 8 public class RedissonBloomFilter {
 9 
10     public static void main(String[] args) {
11         Config config = new Config();
12         config.useSingleServer().setAddress("redis://192.168.14.104:6379");
13         config.useSingleServer().setPassword("123");
14         //构造Redisson
15         RedissonClient redisson = Redisson.create(config);
16 
17         RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phoneList");
18         //初始化布隆过滤器:预计元素为100000000L,误差率为3%
19         bloomFilter.tryInit(100000000L,0.03);
20         //将号码10086插入到布隆过滤器中
21         bloomFilter.add("10086");
22 
23         //判断下面号码是否在布隆过滤器中
24         System.out.println(bloomFilter.contains("123456"));//false
25         System.out.println(bloomFilter.contains("10086"));//true
26     }
27 }
技术图片

  这是单节点的Redis实现方式,如果数据量比较大,期望的误差率又很低,那单节点所提供的内存是无法满足的,这时候可以使用分布式布隆过滤器,同样也可以用 Redisson 来实现,这里我就不做代码演示了,大家有兴趣可以试试。

4、guava 工具

  最后提一下不用Redis如何来实现布隆过滤器。

  guava 工具包相信大家都用过,这是谷歌公司提供的,里面也提供了布隆过滤器的实现。

技术图片
 1 package com.ys.rediscluster.bloomfilter;
 2 
 3 import com.google.common.base.Charsets;
 4 import com.google.common.hash.BloomFilter;
 5 import com.google.common.hash.Funnel;
 6 import com.google.common.hash.Funnels;
 7 
 8 public class GuavaBloomFilter {
 9     public static void main(String[] args) {
10         BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),100000,0.01);
11 
12         bloomFilter.put("10086");
13 
14         System.out.println(bloomFilter.mightContain("123456"));
15         System.out.println(bloomFilter.mightContain("10086"));
16     }
17 }
技术图片

 

以上是关于redis中布隆过滤器使用详解的主要内容,如果未能解决你的问题,请参考以下文章

Redis详解布隆过滤器和缓存穿透解决方案,你能知道有多少?看完这篇文章,你就能搞懂!

高级数据结构 ---- 跳跃表布隆过滤器一致性哈希雪花算法

高级数据结构 ---- 跳跃表布隆过滤器一致性哈希雪花算法

Redis详解(十三)------ Redis布隆过滤器

Redis详解(十三)------ Redis布隆过滤器

Redis--详解布隆过滤器和缓存穿透解决方案