布隆过滤器的原理与实现

        前几天看视频,无意中看到一个Redis缓存会存在三大问题,分别是缓存穿透,缓存击穿,缓存雪崩。在处理缓存穿透的时候,介绍了两种方案,推荐使用布隆过滤器,还有一个是Redis中缓存null。之前只是有听过布隆过滤器,但是对其原理和特点,使用场景并不熟悉。于是网上查阅了挺多资料。总结成为自己看得懂的一篇个人对于布隆过滤器的理解文章。

        布隆过滤器,英文 Bloom Filter。简单理解是一个叫布隆的人开发的一个过滤器,过滤器我的理解就是为了进行逻辑上的过滤校验。布隆过滤器是一个用于校验巨量数据中是否存在某个值。布隆过滤器校验的结果特点是:

  • 若过滤器判断某个元素存在,那么元素不一定存在
  • 若过滤器判断某个元素不存在,那么元素就一定不存在

其中存在不存在的理解是指:需要校验的值是否在布隆过滤器中存在映射关系。

下面通过布隆过滤器数据结构与原理,实际Java代码示例来巩固理解。

1. 数据结构与原理

        我理解的布隆过滤器的数据结构是bit类型的map,值通过计算hash值,获取bit数组中对应的下标,hash值和bit数组下标之间有map的映射关系。映射关系对应的bit数组的值由0改为1。图示如下:

根据图片来理解其实现的原理,其中URL1,URL2,URL3就是需要在bit数组中需要建立映射关系的数据,hash1,hash2,hash3就是值经过hash函数计算出来的下标值,计算出下标值后,对应的下标就将值由原来的0改为1,就是图中绿色所显示的部分。其实我对布隆过滤器的原理理解就是这么短短的几句话总结完了。详细总结:其中我将需要建立关系的值定义为key

  • 创建一个bit数组,bit数组中的值都初始化为0
  • 将需要建立映射关系的key在建立映射关系前都需要进行多次的hash函数计算【使用多次hash函数计算是为了减少hash冲突,提交验证存在的准确率
  • 得到hash之后,在根据计算规则,得到key位于bit数组中的下标
  • 在得到bit数组下标后,就下标对应的值改为1
  • 将所有需要建立映射关系的key重复刚才的逻辑,建立好映射关系
  • 将需要检验的key进过刚才的hash函数计算出该key对应的bit数组下标,判断该下标对应的值是否为1
  • 如果bit数组中下标对应的值为1,布隆过滤器就认为校验的key存在,如果bit数组对应的值为0,布隆过滤器就认为校验的key不存在

为什么校验存在的话,实际可能是不存在呢?因为hash存在hash冲突导致结果不准!

2. 布隆过滤器的实现

        我在网上看存在三种实现方式,我认为其中两种都是结合redis实现,我就将这两种归为一种。我的实现是基于redis中的一种,感兴趣的可以查阅相关资料。

2.1 Guava实现布隆过滤器

代码如下:

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;

/**
 * @Author alen
 * @DATE 2022/4/19 22:51
 * 布隆过滤器测试
 */
public class BloomFilterTest {

    /**
     * 设置需要插入的数据量,100万条数据
     */
    private static final int insertData = 1000000;

    /**
     * 设置误判率
     */
    private static final double errorRatio = 0.02;

    public static void main(String[] args) {
        //初始化一个存储String类型的布隆过滤器,误判率设置为0.02,默认值是0.03
        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertData, errorRatio);
        //用于存放所有的key,现实场景中该数据来源于数据库,作用是用来判断是否存在
        Set<String> setList = new HashSet<>(insertData);
        //用于存放所有实际存在的key,用于取出
        List<String> lists = new ArrayList<>(insertData);
        //创建模拟数据
        for (int i = 0; i < insertData; i++) {
            String uuid = UUID.randomUUID().toString();
            /**
             * 向布隆过滤器中添加元素,跟踪put源码,里面有进行多次hash计算
             */
            bloomFilter.put(uuid);
            setList.add(uuid);
            lists.add(uuid);
        }
        //统计判断实际存在的总个数
        int rightCnt = 0;
        //统计误以为存在实际不存在的个数
        int wrongCnt = 0;

        for (int i = 0; i < 1000; i++) {
            String str = i % 100 == 0 ? lists.get(i / 100) : UUID.randomUUID().toString();
            //mightContain(): 判断布隆过滤器中元素是否存在
            if (bloomFilter.mightContain(str)) {
                if (setList.contains(str)) {
                    rightCnt ++;
                    continue;
                }
                wrongCnt ++;
            }
        }
        BigDecimal percent = new BigDecimal(wrongCnt).divide(new BigDecimal(990), 2, RoundingMode.HALF_UP);
        BigDecimal bingo = new BigDecimal(990 - wrongCnt).divide(new BigDecimal(990), 2, RoundingMode.HALF_UP);
        System.out.println("在1000000个元素中,布隆过滤器认为存在的:" + rightCnt +",误认为存在的:" + wrongCnt + ",命中率:" + bingo + ",误判率:" + percent);
    }
}

        Guava实现的布隆过滤器针有一定的局限性,由于现在基本上都是分布式部署服务,如果使用Guava实现的布隆过滤器,需要每个服务在启动的时候,都需要将目标数据加载到服务中,对内存资源的消耗较大。所以在一般真是的使用中,不会使用这种方式,而是采用基于Redis实现的模式。

2.2 基于Redis实现布隆过滤器

        基于Redis实现布隆过滤器的话,需要我们自己造轮子了,实现代码如下。

1. 创建BloomFilterHelper类,该类是布隆过滤器的核心类,代码详情


import com.google.common.hash.Funnel;
import com.google.common.hash.Funnels;
import com.google.common.hash.Hashing;

import java.nio.charset.Charset;

/**
 * @Author alen
 * @DATE 2022/4/20 9:18
 * Bloom Filter的核心类
 */
public class BloomFilterHelper<T> {

    /**
     * 定义hash函数执行次数
     */
    private int numHashFunctions;
    /**
     * bit数组大小
     */
    private int bitSize;
    /**
     * 数据类型
     */
    private Funnel<T> funnel;

    /**
     * 设置默认的误判率
     */
    private static final double errorRatio = 0.02;

    /**
     * 无参构造器
     */
    public BloomFilterHelper() {

    }

    /**
     * 构造方法
     * @param insertions 需要插入的数据条数
     */
    public BloomFilterHelper(int insertions) {
        this.funnel = (Funnel<T>) Funnels.stringFunnel(Charset.defaultCharset());
        bitSize = optimalBitsLength(insertions, errorRatio);
        numHashFunctions = optimalHashFunctionsCnt(insertions, bitSize);
    }

    /**
     * 构造方法
     *
     * @param funnel 数据类型
     * @param insertions 插入的数据条数
     * @param errorRatio 误判率
     */
    public BloomFilterHelper(Funnel<T> funnel, int insertions, double errorRatio) {
        this.funnel = funnel;
        bitSize = optimalBitsLength(insertions, errorRatio);
        numHashFunctions = optimalHashFunctionsCnt(insertions, bitSize);
    }

    /**
     * hash计算
     * 计算多次hash对应的下标
     *
     * @param value 需要hash的值
     * @return 偏移量数组
     */
    public int[] offsetArray(T value) {
        int[] offset = new int[numHashFunctions];
        long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
        int hash1 = (int) hash64;
        int hash2 = (int) (hash64 >>> 32);
        for (int i = 1; i <= numHashFunctions; i++) {
            int nextHash = hash1 + i * hash2;
            if (nextHash < 0) {
                nextHash = ~nextHash;
            }
            offset[i - 1] = nextHash % bitSize;
        }
        return offset;
    }

    /**
     * 计算最佳的bit数组长度
     *
     * @param insertions 需要插入的数据总数
     * @param errorRatio 误判率
     * @return bit数组长度
     */
    private int optimalBitsLength(long insertions, double errorRatio) {
        if (errorRatio == 0) {
            errorRatio = 0.03;
        }
        return (int) (-insertions * Math.log(errorRatio) / (Math.log(2) * Math.log(2)));
    }

    /**
     * 计算hash方法执行的最佳次数
     *
     * @param insertions 插入数据总数
     * @param bitSize bit数组大小
     * @return 最佳hash次数
     */
    private int optimalHashFunctionsCnt(long insertions, long bitSize) {
        return Math.max(1, (int) Math.round((double) bitSize / insertions * Math.log(2)));
    }
}

2. 创建RedisBloomFilter类,操作布隆过滤器,详情如下


import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

/**
 * @Author alen
 * @DATE 2022/4/20 19:40
 * redis操作布隆过滤器
 */
@Component
public class RedisBloomFilter {

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 删除缓存的key
     *
     * @param key 需要删除的key
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }

    /**
     * 将值在布隆过滤器中添加映射关系
     * 一个一个元素添加时使用
     *
     * @param bloomFilterHelper 布隆过滤器对象
     * @param key key
     * @param value value
     * @param <T> 泛型
     */
    public <T> void add(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        int[] offsetArray = bloomFilterHelper.offsetArray(value);
        for (int offset : offsetArray) {
            //setBit命令;
            redisTemplate.opsForValue().setBit(key, offset, true);
        }
    }

    /**
     * 在布隆过滤器中添加值
     *
     * @param bloomFilterHelper 布隆过滤器
     * @param key key
     * @param valueList 值的集合
     * @param <T> 泛型
     */
    public <T> void addList(BloomFilterHelper<T> bloomFilterHelper, String key, List<T> valueList) {
        redisTemplate.executePipelined(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
                //开启连接
                redisConnection.openPipeline();
                for (T value : valueList) {
                    //hash计算
                    int[] offsetArray = bloomFilterHelper.offsetArray(value);
                    for (int offset : offsetArray) {
                        redisConnection.setBit(key.getBytes(), offset, true);
                    }
                }
                redisConnection.close();
                return null;
            }
        });
    }

    /**
     * 判断值是否存在
     *
     * @param bloomFilterHelper 布隆过滤器
     * @param key key
     * @param value 值
     * @param <T> 泛型
     * @return
     */
    public <T> boolean isExists(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        int[] offsetArray = bloomFilterHelper.offsetArray(value);
        for (int offset : offsetArray) {
            //判断bit位的值是否为1,
            if (!redisTemplate.opsForValue().getBit(key, offset)) {
                return false;
            }
        }
        return true;
    }
}

以上就是布隆过滤器基于Redis实现的全部代码,测试部分可以自己写一个controller测试一下!

3. Spring Boot中使用布隆过滤器

3.1 引入redisson的jar包

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--redisSon-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.14.1</version>
        </dependency>

3.2 bootstrap.yml中添加redis配置

spring:
   redis:
    host: 127.0.0.1
    port: 6379
    password: xxxxxx
    database: 0
    timeout: 1000

# 布隆过滤器配置
bloomfilter:
  expected-insertions: 1000000
  false-probability: 0.02

3.3 创建Redisson配置文件


import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author alen
 * @DATE 2022/3/27 11:13
 */
@Configuration
public class RedisSonConfiguration {

    @Value("${spring.redis.host}")
    private String address;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.database}")
    private String database;

    @Value("${spring.redis.timeout}")
    private String timeout;
    //预估需要插入的值
    @Value("${bloomfilter.expected-insertions}")
    private Long expectedInsertions;
    //容错率
    @Value("${bloomfilter.false-probability}")
    private double falseProbability;

    @Bean
    public RedissonClient redisson() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress(address())
                .setPassword(password)
                .setDatabase(Integer.parseInt(database))
                .setConnectTimeout(Integer.parseInt(timeout));
        return Redisson.create(config);
    }

    /**
     * 注入布隆过滤器
     * @return
     */
    @Bean
    public RBloomFilter bloomFilter() {
        RBloomFilter<Object> bloomFilter = redisson().getBloomFilter("bloom-filter");
        bloomFilter.tryInit(expectedInsertions, falseProbability);
        return bloomFilter;
    }

    /**
     * 生成address
     * @return
     */
    private String address() {
        return "redis://" + address + ":" + port;
    }
}

3.4  布隆过滤器中加载数据


import com.body.park.user.config.RedisSonConfiguration;
import com.body.park.user.entity.TUser;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author alen
 * @DATE 2022/4/21 9:01
 */
@Slf4j
@Component
public class InitBloomFilterData implements ApplicationRunner {

    @Autowired
    private RedisSonConfiguration redisSonConfiguration;

    private RBloomFilter<Long> bloomFilter;

    @PostConstruct
    public void init() {
        bloomFilter = redisSonConfiguration.bloomFilter();
    }

    /**
     *
     * @param args
     * @throws Exception
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        List<TUser> userList = new ArrayList<>();
        //模拟数据,真是数据从数据库中查询
        for (long i = 0; i < 1000L; i++) {
            TUser user = new TUser();
            user.setId(i);
            userList.add(user);
        }
        userList.forEach(tUser -> {
            bloomFilter.add(tUser.getId());
        });
    }
}

3.5 在业务逻辑中使用

/**
 * @Author alen
 * @DATE 2022/3/13 20:32
 */
@DubboService
@Component
@Slf4j
public class UserTestServiceImpl implements UserTestService {

    @Autowired
    private RedisBloomFilter redisBloomFilter;

    @Autowired
    private RedisSonConfiguration redisSonConfiguration;

    private RBloomFilter<Long> bloomFilter;

    @PostConstruct
    public void init() {
        bloomFilter = redisSonConfiguration.bloomFilter();
    }

    @Override
    public boolean checkBloomFilterData(Long value) {
        boolean flag = bloomFilter.contains(value);
        log.info("值:{},是否存在布隆过滤器中:{}", value, flag);
        return flag;
    }
}

然后写一个controller测试一下即可,到此所有的布隆过滤器相关的都已经梳理完成,有不足之处,欢迎讨论!

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值