秒杀项目中布隆过滤器的使用和坑

2019-12-21  本文已影响0人  就这些吗

贴一下我的其他文章:
基于Springboot和redis的秒杀业务实现(项目里面有项目源码地址和项目分析)
redis中的布隆过滤器(介绍了什么是布隆过滤器,原理及其优缺点)

1.布隆过滤器加进去之后很难删除
2.布隆过滤器也会有误差,能保证存在的100%去查询redis或者数据库,只能保证99%的恶意请求不会影响到数据库,剩下的1%就是布隆过滤器没拦截到的。

1.在pom.xml添加谷歌的工具类guava的jar包


44.png

2.创建一个BloomFilterHelper类,待会初始化的时候会用到。

package guhao.utils;

import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;

public class BloomFilterHelper<T> {

    private int numHashFunctions;

    private int bitSize;

    private Funnel<T> funnel;

    public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
        Preconditions.checkArgument(funnel != null, "funnel不能为空");
        this.funnel = funnel;
        // 计算bit数组长度
        bitSize = optimalNumOfBits(expectedInsertions, fpp);
        // 计算hash方法执行次数
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
    }

    public int[] murmurHashOffset(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数组长度
     */
    private int optimalNumOfBits(long n, double p) {
        if (p == 0) {
            // 设定最小期望长度
            p = Double.MIN_VALUE;
        }
        int sizeOfBitArray = (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
        return sizeOfBitArray;
    }

    /**
     * 计算hash方法执行次数
     */
    private int optimalNumOfHashFunctions(long n, long m) {
        int countOfHash = Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
        return countOfHash;
    }
}

3.在RedisTemplateConfig类中初始化布隆过滤器,并交给Spring管理,这里用的是lambda表达式,注意这里会直接复制初始化的代码会报错,是导包的问题,先把需要的guava工具包导上就ok了。

package guhao.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import com.google.common.hash.Funnel;
import guhao.utils.BloomFilterHelper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;


@Configuration
public class RedisTemplateConfig {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        logger.info("RedisTemplate序列化配置,转化方式:" + jackson2JsonRedisSerializer.getClass().getName());
        return redisTemplate;
    }
    
    @Bean
    public BloomFilterHelper<String> initBloomFilterHelper() {
        return new BloomFilterHelper<>((Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8).putString(from, Charsets.UTF_8), 1000000, 0.01);
    }

}

4.添加一个操作布隆过滤器的类,里面写了往过滤器里添加值和判断值是否存在的两个方法

package guhao.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import com.google.common.base.Preconditions;

import guhao.utils.BloomFilterHelper;

@Service
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根据给定的布隆过滤器添加值
     */
    public <T> void addByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
//            System.out.println("key : " + key + " " + "value : " + i);
            redisTemplate.opsForValue().setBit(key, i, true);
        }
    }

    /**
     * 根据给定的布隆过滤器判断值是否存在
     */
    public <T> boolean includeByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
//            System.out.println("key : " + key + " " + "value : " + i);
            if (!redisTemplate.opsForValue().getBit(key, i)) {
                return false;
            }
        }

        return true;
    }

}

5.接下来就是在业务里使用我们的布隆过滤器了,业务代码各不相同也没法复用,我就用图片贴出来我的用法了。

1.上面的框是我们刚才创建和修改的类,都创建出来交由spring管理
2.在数据加入到redis的时候,也加入到布隆过滤器中

3.注意这里我们写加入布隆过滤器方法的时候在初始化布隆过滤器的时候就写好了类型,所以这边要将seckillId的Long类型转成String

bbb.png
aaa.png

在想要查询的时候先看看布隆过滤器里有没有这个值,有的话就去查(注意,这里我们是把数据库所有的数据都加到布隆过滤器里啦,所以能判断这个数据合不合法,不合法的直接到else里去了,返回一个空值,或者没准备好,这边没截出来,代码太长了,意会一下即可)最后我们可以用一下postman测试一下这个布隆过滤器到底有没有生效。

ccc.png

用postman测试接口,先试试在布隆过滤器中存在的值(布隆过滤器中只有1,2,3)
1.调用接口,发现过了if,查询落到了redis或数据库上,返回了正确的值


1.png
2.png
3.png
3.png

2.调用接口,这次传没有在布隆过滤器里的值,发现直接到了else,返回了默认值,至此我们可以说,布隆过滤器,成功为我所用矣


44.png
5.png
6.png
上一篇 下一篇

猜你喜欢

热点阅读