Redis系列--布隆过滤器(Bloom Filter)

一、前言

在实际开发中,会遇到很多要判断一个元素是否在某个集合中的业务场景,类似于垃圾邮件的识别,恶意ip地址的访问,缓存穿透等情况。类似于缓存穿透这种情况,有许多的解决方法,如:redis存储null值等,而对于垃圾邮件的识别,恶意ip地址的访问,我们也可以直接用 HashMap 去存储恶意ip地址以及垃圾邮件,然后每次访问时去检索一下对应集合中是否有相同数据。这种思路对于数据量小的项目来说是没有问题的,但是对于大数据量的项目,如,垃圾邮件出现有十几二十万,恶意ip地址出现有上百万,或者从几十亿电话中检索出指定的电话是否在等操作,那么这十几亿的数据就会占据大几G的空间,这个时候就可以考虑一下布隆过滤器了。

二、布隆过滤器(Bloom Filter)

一、是什么

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

一句话就是:由一个初始值为零的bit数组和多个哈希函数构成,用来快速判断集合中是否存在某个元素。

 使用bit数组的目的就是减少内存的占用,数组不保存数据信息,只是在内存中存储一个是否存在的表示0或1

二、原理

一、原理

当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个位阵列(Bit array)中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了。

1、添加key

使用多个hash函数对key进行hash运算得到多个整数索引值,对位数组长度进行取模运算得到多个位置,每个hash函数都会得到一个不同的位置,将这几个位置都置1就完成了add操作。

例如,我们添加一个字符串wmyskxz,对字符串进行多次hash(key) → 取模运行→ 得到坑位

 2、查询key

将这个key的多个位置上的值取出来,只要有其中一位是零就表示这个key不存在,但如果都是1,则不一定存在对应的key。(也就是有,不一定有,无,就一定无)

比如我们在 add 了字符串wmyskxz数据之后,很明显下面1/3/5 这几个位置的 1 是因为第一次添加的 wmyskxz 而导致的;

此时我们查询一个没添加过的不存在的字符串inexistent-key,它有可能计算后坑位也是1/3/5 ,这就是误判了

二、hash冲突导致数据不精准

当有变量被加入集合时,通过N个映射函数将这个变量映射成位图中的N个点,

把它们置为 1(假定有两个变量都通过 3 个映射函数)。

 为什么说有,不一定有,无,就一定无。那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。如上图,obj1和obj2放入的位置都是相同的,如果只放入obj2不放入obj1,然后查key为obj1的也是能够查到bit数组上都是1的结果,但这并不代表obj1就存在。

1、hash函数 

将任意大小的输入数据转换成特定大小的输出数据的函数,转换后的数据称为哈希值或哈希编码,也叫散列值。

如果两个散列值是不相同的(根据同一函数)那么这两个散列值的原始输入也是不相同的。

这个特性是散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。

散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同,

这种情况称为“散列碰撞(collision)”。

 

用 hash表存储大数据量时,空间效率还是很低,当只有一个 hash 函数时,还很容易发生哈希碰撞。

2、hash冲突代码复现

package test;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * @Description : 哈希冲突复现
 * @Author : hc
 * @Date : 2023/6/14 19:02
 **/
public class HashCodeTest {
    public static void main(String[] args) {

        Set<Integer> hashCodeSet = new HashSet<>();

        for (int i = 0; i < 200000; i++) {
            int hashCode = new Object().hashCode();
            if (hashCodeSet.contains(hashCode)) {
                System.out.println("出现了重复的hashcode: " + hashCode + "\t 运行到" + i);
                break;
            }
            hashCodeSet.add(hashCode);
        }
        System.out.println("Aa".hashCode());
        System.out.println("BB".hashCode());
        System.out.println("柳柴".hashCode());
        System.out.println("柴柕".hashCode());
    }
}

结果:
出现了重复的hashcode: 2134400190     运行到105084
2112
2112
851553
851553

三、布隆过滤器删除问题

原因:

布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,

因此误判的根源在于相同的 bit 位被多次映射且置 1。

导致结果:

这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。

如果我们直接删除这一位的话,会影响其他的元素

特性:

布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。只能重构。

总结:

1、使用时进行布隆过滤器的初始化,一次性给够容量,不要让实际数量大于初始化数量,避免重构布隆过滤器。

2、如果实际数量大于初始化数量,这个时候就需要进行重构了,重新分配一个更大数量的过滤器,再将所有旧数据重新初始化进过滤器。

三、使用场景

一、黑白名单校验、识别垃圾邮件

发现存在黑名单中的,就执行特定操作。比如:识别垃圾邮件,只要是邮箱在黑名单中的邮件,就识别为垃圾邮件。

 

假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。

把所有黑名单都放在布隆过滤器中,在收到邮件时,判断邮件地址是否在布隆过滤器中即可。

二、解决缓存穿透问题 

把已存在数据的key存在布隆过滤器中,相当于redis前面挡着一个布隆过滤器。

 

当有新的请求时,先到布隆过滤器中查询是否存在:

如果布隆过滤器中不存在该条数据则直接返回;

如果布隆过滤器中已存在,才去查询缓存redis,如果redis里没查询到则再查询Mysql数据库

四、布隆过滤器优缺点 

优点:高效插入和查询,内存占用空间少

缺点:

1、存在误判,不能精确过滤

2、不能删除元素

三、代码实现

一、手动实现布隆过滤器

/**
 * @Description : 布隆过滤器白名单初始化
 * 1、初始化一部分数据进入到布隆过滤器
 * 2、新增数据的时候如果数据库中没有,新增成功后,加入数据到布隆过滤器
 * @Author : hc
 * @Date : 2023/6/14 22:00
 **/
@Slf4j
@Component
public class BloomFilterInit {

    // 假设这是初始化数据
    private static final String UID = "user:12";
    // 白名单key
    public static final String WHITELIST_USER_KRY = "whitelist:user:";

    @Resource
    private CheckUtils checkUtils;
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 白名单用户信息加载
     * @author hc
     * @date 2023/6/15 11:45
     */
    @PostConstruct
    public void init() {
        // 1、获取hashCOde,由于可能出现负数,取绝对值
        int abs = Math.abs(UID.hashCode());
        // 2、直接设置布隆过滤器的bit数组为2的32次方,这里只使用一个hash函数与一个bit位置的数值进行演示
        long index = checkUtils.getIndex(abs);
        // 3、使用redis新数据类型bitmap进行存储,key=WHITELIST_USER_KRY,偏移量表示这个bit数组的下标,value设置为true表示1
        redisTemplate.opsForValue().setBit(WHITELIST_USER_KRY,index,Boolean.TRUE);
    }

}
/**
 * @Description :布隆过滤器校验工具
 * @Author : hc
 * @Date : 2023/6/15 11:34
 **/
@Slf4j
@Component
public class CheckUtils {

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 布隆过滤器校验
     *
     * @param key
     * @return boolean
     * @author hc
     * @date 2023/6/15 11:42
     */
    public boolean checkData(String key) {
        int abs = Math.abs(key.hashCode());
        long index = (long) (abs % Math.pow(2, 32));
        return redisTemplate.opsForValue().getBit(BloomFilterInit.WHITELIST_USER_KRY, index);
    }

    /**
     * 获取偏移量
     * @param key
     * @return long
     * @author hc
     * @date 2023/6/15 17:19
     */
    public long getOffsetId(String key) {
        int abs = Math.abs(key.hashCode());
        return getIndex(abs);
    }

    /**
     * 计算偏移量
     *
     * @param abs
     * @return java.lang.Long
     * @author hc
     * @date 2023/6/15 16:25
     */
    public long getIndex(int abs) {
        if (0 == abs) {
            return 0L;
        }
        return (long) (abs % Math.pow(2, 32));
    }
}

/**
 * @Description :
 * @Author : hc
 * @Date : 2023/6/14 21:25
 **/
@Slf4j
@Service
public class BloomFilterService {

    private static final String CACHE_KEY_USER = "user:";

    @Resource
    private CheckUtils checkUtils;
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private BloomFilterDao bloomFilterDao;

    @Transactional(rollbackFor = Exception.class)
    public void addUser(User user) {
        // 返回技术主键雪花id
        long i = bloomFilterDao.addUser(user);
        // 这里可以开启一个异步线程,在事务提交之后再进行操作
        if (0 < i) {
            String key = CACHE_KEY_USER.concat(String.valueOf(user.getId()));
            long index = checkUtils.getOffsetId(key);

            // redis的数据都需要使用统一的json工具转成json格式后放入
            String userJson = JSONUtil.toJsonStr(user);
            redisTemplate.opsForValue().set(key, userJson);
            redisTemplate.opsForValue().setBit(BloomFilterInit.WHITELIST_USER_KRY, index, Boolean.TRUE);
            log.info("新增用户信息|用户key:{}|布隆过滤器偏移量:{}", key, index);
        }
    }

    public User queryUser(Long id) {
        if (0 > id) {
            log.info("获取用户信息|用户id异常,异常id:{}", id);
            return null;
        }

        String key = CACHE_KEY_USER.concat(String.valueOf(id));
        boolean checkData = checkUtils.checkData(key);
        if (!checkData) {
            log.info("获取用户信息|用户id不存在,异常id:{}", id);
            return null;
        }

        User user = (User) redisTemplate.opsForValue().get(key);
        if (Objects.isNull(user)) {
            // 这里可以换成分布式锁
            synchronized (this) {
                user = (User) redisTemplate.opsForValue().get(key);
                if (Objects.isNull(user)) {
                    user = bloomFilterDao.queryUser(id);
                }
                if (Objects.nonNull(user)) {
                    long index = checkUtils.getOffsetId(key);
                    String userJson = JSONUtil.toJsonStr(user);
                    redisTemplate.opsForValue().set(key, userJson);
                    redisTemplate.opsForValue().setBit(BloomFilterInit.WHITELIST_USER_KRY, index, Boolean.TRUE);
                }
            }
        }
        return user;
    }

}

二、使用guava单机版实现布隆过滤器

1、误差率

/**
 * @Description :
 * @Author : hc
 * @Date : 2023/6/15 20:56
 **/
@Service
@Slf4j
public class GuavaBloomFilterService {

    //布隆过滤器里预计要插入多少数据
    public static int SIZE = 1000000;
    //误判率,它越小误判的个数也就越少(但是越小所消耗的资源就越多),这个数是谷歌布隆过滤器默认的值
    //fpp the desired false positive probability
    public static double FPP = 0.03;

    public static void main(String[] args) {
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE, FPP);

        //1 先往布隆过滤器里面插入100万的样本数据
        for (int i = 1; i <= SIZE; i++) {
            bloomFilter.put(i);
        }
        //故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里
        List<Integer> list = new ArrayList<>(SIZE);
        for (int i = SIZE + 1; i <= SIZE + (100000); i++) {
            if (bloomFilter.mightContain(i)) {
                log.info("被误判了:{}", i);
                list.add(i);
            }
        }
        log.info("误判的总数量::{}", list.size());
    }
}

误判率为:0.03

BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size,0.01); 误伤的数量:100

2、使用

/**
 * @Description :
 * @Author : hc
 * @Date : 2023/6/15 22:15
 **/
public class GuavaBloomFilterUtils {

    private final static BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(UTF_8), 100000000, 0.01);


    public static boolean isExist(String id) {
        return bloomFilter.mightContain(id);
    }

    public static void put(String id) {
        bloomFilter.put(id);
    }
}
/**
 * @Description :
 * @Author : hc
 * @Date : 2023/6/16 9:48
 **/
@Slf4j
@Configuration
public class GuavaBloomFilterInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        /**
         * 请求方式是OPTIONS说明是第一次,前端请求一次浏览器那边有两次请求,
         * 第一次请求方法携带OPTIONS,类似于先过来询问后端能不能连接,如果可以,则它会在HTTP头中包含一个名为“Allow”的头返回。
         * 第二次请求才是get、post等真正的请求。
         */
        if (HttpMethod.OPTIONS.matches(request.getMethod())) {
            response.setStatus(HttpStatus.OK.value());
            return Boolean.TRUE;
        }

        String dataStr = null;
        // 所有请求均使用post方式,if尽量不要嵌套进去
        if (HttpMethod.POST.matches(request.getMethod())) {
            dataStr = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));

        }
        if (StrUtil.isEmpty(dataStr)) {
            resData(response, HttpStatus.CONTINUE.value());
            return Boolean.FALSE;
        }
        // 假设是去其中的id
        JSONObject jsonObject = JSONUtil.parseObj(dataStr);
        String id = (String) jsonObject.get("id");

        if (StrUtil.isNotEmpty(id) && GuavaBloomFilterUtils.isExist(id)) {
            return Boolean.TRUE;
        }
        resData(response,HttpStatus.CONTINUE.value());
        return Boolean.FALSE;
    }

    /**
     * 统一使用HttpStatus.CONTINUE.value(),方便前端判断跳转指定页面
     * @param response
     * @param status
     */
    private static void resData(HttpServletResponse response, int status) {
        response.setStatus(status);
        response.setHeader("Content-Type", "application/json");
        response.setCharacterEncoding("UTF-8");
        log.info("布隆过滤器校验|数据不存在");
    }
}
/**
 * @Description :
 * @Author : hc
 * @Date : 2023/6/16 11:05
 **/
@Configuration
public class GuavaBloomFilterConfig implements WebMvcConfigurer {

    @Resource
    private GuavaBloomFilterInterceptor guavaBloomFilterInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截所有请求,这里需要放行登录、注册等相关接口。
        registry.addInterceptor(guavaBloomFilterInterceptor).addPathPatterns("/**");
    }

}

缺点:

1、基于本地缓存(jvm),容量受限制

2、多个应用就有多个布隆过滤器,多应用同步复杂。

三、redis分布式布隆过滤器的实现

二中的使用是基于jvm的,难以用到分布式系统中,如果想要应用于分布式系统中,就需要加入redis,使用redis的setBit命令即可对对应key设置bit位。也即是一、手动实现布隆过滤器中的实现。

可以参照guava版布隆过滤器源码

/**
 * @Description :思路:可以直接拿guava包里的源码进行修改
 * 根据布隆过滤器原理
 * 1、首先需要有k个函数,用来计算key对应的hash值,key与函数的关系是一对多
 * 2、需要初始化一个N位的bit数组
 * 3、新增key时,需要通过多个hash值对数组大小取余,找到对应多个位置,然后置为1
 * 4、判断key是否在布隆过滤器中,用k个hash函数计算出k个散列值,并计算出对应的数组下表,
 * 查询数组中对应的数据,如果所有的比特位都是1,认为在集合中。
 * @Author : hc
 * @Date : 2023/6/16 12:26
 **/
public class BloomFilterHelper<T> {

    private Long bitSize; // 二进制数组大小
    private int numHashFunctions; // hash函数个数
    private Funnel<T> funnel; // 可自定义,如果只是String,Long等普通类型可以直接使用guava中的即可

    /**
     * @param expectedInsertions 预估插入数据数量
     * @param fpp                允许数据误差率
     * @param funnel
     */
    public BloomFilterHelper(Long expectedInsertions, double fpp, Funnel<T> funnel) {
        this.funnel = funnel;
        bitSize = optimalNumOfBits(expectedInsertions, fpp);
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
    }

    /**
     * 计算bit数组大小
     *
     * @param n 预估插入数据数量
     * @param p 允许数据误差率
     * @return
     */
    private long optimalNumOfBits(long n, double p) {
        if (p == 0) p = Double.MIN_VALUE;
        return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
    }

    /**
     * 计算hash函数个数
     *
     * @param n 预估插入数据数量
     * @param m bit数组大小
     * @return
     */
    private int optimalNumOfHashFunctions(long n, long m) {
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
    }

    /**
     * 计算元素的hash散列下标
     *
     * @param value 元素
     * @return
     */
    public Long[] mightContain(T value) {
        Long[] longs = new Long[numHashFunctions];
        long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
        int hash1 = (int) hash64;
        int hash2 = (int) (hash64 >>> 32);
        // 循环hash函数,对数组取余,得到多个数组下标
        for (int i = 1; i <= numHashFunctions; ++i) {
            int combinedHash = hash1 + i * hash2;
            if (combinedHash < 0) {
                combinedHash = ~combinedHash;
            }
            longs[i - 1] = combinedHash % bitSize;
        }
        return longs;
    }

}
/**
 * @Description :
 * @Author : hc
 * @Date : 2023/6/16 16:10
 **/
@Configuration
public class RedisBloomFilterUtils {

    private static final Long SIZE = 100000000L;
    private static final Double FPP = 0.01;


    @Resource
    private RedisTemplate redisTemplate;

    private static final BloomFilterHelper bloomFilterHelper = new BloomFilterHelper(SIZE, FPP, Funnels.stringFunnel(Charsets.UTF_8));

    /**
     * 布隆过滤器新增数据
     *
     * @param key
     * @author hc
     * @date 2023/6/16 16:31
     */
    public void put(String key) {
        Long[] indexArray = getIndexArray(key);
        Arrays.stream(indexArray).filter(Objects::nonNull).forEach(index -> redisTemplate.opsForValue().setBit(key, index, Boolean.TRUE));
    }

    /**
     * 检查布隆过滤器中是否存在
     *
     * @param key
     * @return boolean
     * @author hc
     * @date 2023/6/16 16:42
     */
    public boolean mightContain(String key) {
        Long[] indexArray = getIndexArray(key);
        return !Arrays.stream(indexArray).filter(Objects::nonNull).anyMatch(index -> Boolean.FALSE == redisTemplate.opsForValue().getBit(key, index));
    }

    private static Long[] getIndexArray(String key) {
        Assert.isFalse(StrUtil.isEmpty(key), "布隆过滤器新增数据|key为空");
        // 获取数组下标
        return bloomFilterHelper.mightContain(key);
    }
}
/**
 * @Description : 初始化数据
 * @Author : hc
 * @Date : 2023/6/16 17:28
 **/
@Slf4j
@Configuration
public class RedisBloomFilterInit implements InitializingBean {

    private static final String PRE_KEY = "user:";

    @Resource
    private RedisBloomFilterUtils redisBloomFilterUtils;

    @Override
    public void afterPropertiesSet() throws Exception {
        List<String> list = Lists.newArrayList("1", "2");
        log.info("加载数据到布隆过滤器,size:{}", list.size());
        list.stream().filter(Objects::nonNull).forEach(id -> {
            String key = PRE_KEY.concat(id);
            redisBloomFilterUtils.put(key);
        });
    }
}
/**
 * @Description :
 * @Author : hc
 * @Date : 2023/6/16 9:48
 **/
@Slf4j
@Configuration
public class RedisBloomFilterInterceptor implements HandlerInterceptor {

    private static final String PRE_KEY = "user:";

    @Resource
    private RedisBloomFilterUtils redisBloomFilterUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        /**
         * 请求方式是OPTIONS说明是第一次,前端请求一次浏览器那边有两次请求,
         * 第一次请求方法携带OPTIONS,类似于先过来询问后端能不能连接,如果可以,则它会在HTTP头中包含一个名为“Allow”的头返回。
         * 第二次请求才是get、post等真正的请求。
         */
        if (HttpMethod.OPTIONS.matches(request.getMethod())) {
            response.setStatus(HttpStatus.OK.value());
            return Boolean.TRUE;
        }

        String dataStr = null;
        // 所有请求均使用post方式,if尽量不要嵌套进去
        if (HttpMethod.POST.matches(request.getMethod())) {
            dataStr = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));

        }
        if (StrUtil.isEmpty(dataStr)) {
            resData(response, HttpStatus.CONTINUE.value());
            return Boolean.FALSE;
        }
        // 假设是去其中的id
        JSONObject jsonObject = JSONUtil.parseObj(dataStr);
        String id = (String) jsonObject.get("id");
        String key = PRE_KEY.concat(id);

        if (StrUtil.isNotEmpty(id) && redisBloomFilterUtils.mightContain(key)) {
            return Boolean.TRUE;
        }
        resData(response,HttpStatus.CONTINUE.value());
        return Boolean.FALSE;
    }

    /**
     * 统一使用HttpStatus.CONTINUE.value(),方便前端判断跳转指定页面
     * @param response
     * @param status
     */
    private static void resData(HttpServletResponse response, int status) {
        response.setStatus(status);
        response.setHeader("Content-Type", "application/json");
        response.setCharacterEncoding("UTF-8");
        log.info("布隆过滤器校验|数据不存在");
    }
}
/**
 * @Description :
 * @Author : hc
 * @Date : 2023/6/16 11:05
 **/
@Configuration
public class RedisBloomFilterConfig implements WebMvcConfigurer {

    @Resource
    private RedisBloomFilterInterceptor redisBloomFilterInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截所有请求,这里需要放行登录、注册等相关接口。
        registry.addInterceptor(redisBloomFilterInterceptor).addPathPatterns("/**");
    }

}

四、利用Redisson实现分布式布隆过滤器

/**
 * @Description :
 * @Author : hc
 * @Date : 2023/6/24 21:20
 **/
@Configuration
public class RedissonBloomFilterInit {

    private static final String BLOOM_FILTER_KEY = "bloom_filter_key:";

    @Resource
    private Redisson redisson;

    @ConditionalOnBean(name = "redisson")
    @Bean("bloomFilter")
    public RBloomFilter<Object> initBloomFilter() {
        RBloomFilter<Object> bloomFilter = getBloomFilter(BLOOM_FILTER_KEY);
        bloomFilter.tryInit(100000000L, 0.01);
        return bloomFilter;
    }

    private RBloomFilter<Object> getBloomFilter(String key) {
        return redisson.getBloomFilter(key);
    }
}
/**
 * @Description :
 * @Author : hc
 * @Date : 2023/6/24 21:00
 **/
@Slf4j
@Component
public class RedissonBloomFilterUtils {

    @Resource
    private RBloomFilter<Object>  bloomFilter;


    public boolean put(String value) {
        if (StringUtil.isEmpty(value)) {
            log.warn("分布式布隆过滤器|value不能为空");
            return false;
        }
        return bloomFilter.add(value);
    }


    public boolean contain(String value) {
        if (StringUtil.isEmpty(value)) {
            log.warn("分布式布隆过滤器|value不能为空");
            return false;
        }
        return bloomFilter.contains(value);
    }
}

四、总结

一、 对布隆过滤器的的总结

无论使用谷歌版本的布隆过滤器还是自己编写的,都会存在两个问题,

1、因为不同元素经过hash函数计算后可能会出现相同的hash值(hash碰撞),就会出现一个误判率的问题

2、因为有hash碰撞,导致同一个位置可能存放不同的数据,这对于删除操作是很不友好的。

对于这些情况可查看另一种布隆过滤器,布谷鸟过滤器

二、由布隆过滤器延伸的思考

 1、如果在项目中初始化一个布隆过滤器,假设大小为10000000,当项目中数据一直在新增,一直布隆过滤器中put值,总有一天hash碰撞的概率会提高,误判率也就随之提高。但是在大数据量的布隆过滤器,进行删除重建,这成本无疑是很高的。

2、对于缓存击穿,如果在使用srpingcache的注解后,可以在主键中配置单个线程访问mysql。

@Cacheable(cacheNames="menu",sync="true")

3、对于使用了springcache注解的,想要解决缓存穿透问题可以,设置返回值为null,

spring.cache.redis.cache-null-values=true

然后时间设置短一点,但是这有个问题就是:如果在设置的null失效的时间内,有大量的请求进来,且查出来的数据也都是null,这个时候,redis内存也会飙升。在这个情况下可以考虑一下布隆过滤器的使用了,加一个黑名单操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/31771.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

宏景eHR SQL注入漏洞复现(CNVD-2023-08743)

0x01 产品简介 宏景eHR人力资源管理软件是一款人力资源管理与数字化应用相融合&#xff0c;满足动态化、协同化、流程化、战略化需求的软件。 0x02 漏洞概述 宏景eHR 存在SQL注入漏洞&#xff0c;未经过身份认证的远程攻击者可利用此漏洞执行任意SQL指令&#xff0c;从而窃取数…

如何在大规模服务中迁移缓存

当您启动初始服务时&#xff0c;通常会过度设计以考虑大量流量。但是&#xff0c;当您的服务达到爆炸式增长阶段&#xff0c;或者如果您的服务请求和处理大量流量时&#xff0c;您将需要重新考虑您的架构以适应它。糟糕的系统设计导致难以扩展或无法满足处理大量流量的需求&…

docker基础

文章目录 通过Vagrant安装虚拟机修改虚拟机网络配置 docker CE安装(在linux上)docker desktop安装(在MacOS上)Docker架构关于-阿里云镜像加速服务配置centos卸载docker 官网: http://www.docker.com 仓库: https://hub.docker.com Docker安装在虚拟机上&#xff0c;可以通过V…

Go语言的TCP和HTTP网络服务基础

目录 【TCP Socket 编程模型】 Socket读操作 【HTTP网络服务】 HTTP客户端 HTTP服务端 TCP/IP 网络模型实现了两种传输层协议&#xff1a;TCP 和 UDP&#xff0c;其中TCP 是面向连接的流协议&#xff0c;为通信的两端提供稳定可靠的数据传输服务&#xff1b;UDP 提供了一种…

[MySQL]不就是SQL语句

前言 本期主要的学习目标是SQl语句中的DDL和DML实现对数据库的操作和增删改功能&#xff0c;学习完本章节之后需要对SQL语句手到擒来。 1.SQL语句基本介绍 SQL&#xff08;Structured Query Language&#xff09;是一种用于管理关系型数据库的编程语言。它允许用户在数据库中存…

双因素身份验证在远程访问中的重要性

在快速发展的数字环境中&#xff0c;远程访问计算机和其他设备已成为企业运营的必要条件。无论是在家庭办公室运营的小型初创公司&#xff0c;还是团队分散在全球各地的跨国公司&#xff0c;远程访问解决方案都能保证工作效率和连接性&#xff0c;能够跨越距离和时间的阻碍。 …

7Z045 引脚功能详解

本文针对7Z045芯片&#xff0c;详细讲解硬件设计需要注意的技术点&#xff0c;可以作为设计和检查时候的参考文件。问了方便实用&#xff0c;按照Bank顺序排列&#xff0c;包含配置Bank、HR Bank、HP Bank、GTX Bank、供电引脚等。 参考文档包括&#xff1a; ds191-XC7Z030-X…

怎么计算 flex-shrink 的缩放尺寸

计算公式: 子元素的宽度 - (子元素的宽度的总和 - 父盒子的宽度) * (某个元素的flex-shrink / flex-shrink总和) 面试问题是这样的下面 left 和 right 的宽度分别是多少 * {padding: 0;margin: 0;}.container {width: 500px;height: 300px;display: flex;}.left {width: 500px…

红日靶场(一)外网到内网速通

红日靶场&#xff08;一&#xff09; 下载地址&#xff1a;http://vulnstack.qiyuanxuetang.net/vuln/detail/2/ win7:双网卡机器 win2003:域内机器 win2008域控 web阶段 访问目标机器 先进行一波信息收集&#xff0c;扫一下端口和目录 扫到phpmyadmin&#xff0c;还有一堆…

【资料分享】Xilinx Zynq-7010/7020工业核心板规格书(双核ARM Cortex-A9 + FPGA,主频766MHz)

1 核心板简介 创龙科技SOM-TLZ7x是一款基于Xilinx Zynq-7000系列XC7Z010/XC7Z020高性能低功耗处理器设计的异构多核SoC工业核心板&#xff0c;处理器集成PS端双核ARM Cortex-A9 PL端Artix-7架构28nm可编程逻辑资源&#xff0c;通过工业级B2B连接器引出千兆网口、USB、CAN、UA…

Triton教程 --- 动态批处理

Triton教程 — 动态批处理 Triton系列教程: 快速开始利用Triton部署你自己的模型Triton架构模型仓库存储代理模型设置优化动态批处理 Triton 提供了动态批处理功能&#xff0c;将多个请求组合在一起执行同一模型以提供更大的吞吐量。 默认情况下&#xff0c;只有当每个输入在…

【开源与项目实战:开源实战】81 | 开源实战三(上):借Google Guava学习发现和开发通用功能模块

上几节课&#xff0c;我们拿 Unix 这个超级大型开源软件的开发作为引子&#xff0c;从代码设计编写和研发管理两个角度&#xff0c;讲了如何应对大型复杂项目的开发。接下来&#xff0c;我们再讲一下 Google 开源的 Java 开发库 Google Guava。 Google Guava 是一个非常成功、…

io.netty学习(十一)Reactor 模型

目录 前言 传统服务的设计模型 NIO 分发模型 Reactor 模型 1、Reactor 处理请求的流程 2、Reactor 三种角色 单Reactor 单线程模型 1、消息处理流程 2、缺点 单Reactor 多线程模型 1、消息处理流程 2、缺点 主从Reactor 多线程模型 主从Reactor 多线程模型示例 1…

CTF-Show密码学:ZIP文件密码破解【暴力破解】

萌新 隐写23 题目内容&#xff1a; 文件的主人喜欢用生日做密码&#xff0c;而且还是个90后。 一、已知条件 在这个题目中&#xff0c;我们有以下已知条件&#xff1a; 文件的主人喜欢用生日做密码 - 这个条件告诉我们&#xff0c;密码可能是一个八位的纯数字密码&#xff0c…

云原生之深入解析如何正确计算Kubernetes容器CPU使用率

一、简介说明 使用 Prometheus 配置 kubernetes 环境中 Container 的 CPU 使用率时&#xff0c;会经常遇到 CPU 使用超出 100%&#xff0c;现在来分析一下&#xff1a; container_spec_cpu_period&#xff1a;当对容器进行 CPU 限制时&#xff0c;CFS 调度的时间窗口&#xff…

[架构之路-214]- UML-类图图解、详解、结构化、本质化讲解

目录 一、什么是类 1.1 概述 1.2 UML中类的表示 1.3 接口 1.4 抽象类 1.5 模板类 二、什么类图 2.1 概述 2.2 类关系 三、UML类图 3.1 结构关系 3.1.1 完全一体&#xff1a;继承关系 &#xff08;类与类耦合度最高&#xff0c;类与类之间最强的关系&#xff09; …

计算机基础知识

参考链接&#xff1a;https://blog.csdn.net/ChineseSoftware/article/details/123176978 https://www.cnblogs.com/8023-CHD/p/11067141.html https://blog.csdn.net/qq_42033567/article/details/108088514 http与https的区别 HTTP 的URL以http:// 开头&#xff0c;而HTTPS…

【Matlab】语音信号分析与处理实验报告

一、目的 使用Matlab分析与设计实验&#xff0c;理解与掌握以下知识点&#xff1a; 1、信号的采样、频谱混叠 2、信号的频谱分析 3、信号的幅度调制与解调方法 4、理想滤波器的时域和频域特性 5、数字滤波器的设计与实现 二、内容 1、录制一段个人的语音信号 2、采用合适的频…

Unity光照贴图的切换,实现黑夜和白天效果

有这么一个需求&#xff0c;不能使用实时光来进行动态控制光照开关&#xff0c;但是又要实现白天和黑夜的效果&#xff0c;我的场景中有大概十几个点光源和平行光 实现步骤&#xff1a; 一、模型原模原样复制到另一个场景中&#xff08;因为贴图只能存在于当前的场景文件夹&am…

支付宝沙箱支付详细教程(IDEA版)—2023最新版

&#x1f607;作者介绍&#xff1a;一个有梦想、有理想、有目标的&#xff0c;且渴望能够学有所成的追梦人。 &#x1f386;学习格言&#xff1a;不读书的人,思想就会停止。——狄德罗 ⛪️个人主页&#xff1a;进入博主主页 &#x1f5fc;专栏系列&#xff1a;无 &#x1f33c…