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

一、前言

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

二、布隆过滤器(Bloom Filter)

一、是什么

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

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

Redis系列--布隆过滤器(Bloom Filter)_第1张图片

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

二、原理

一、原理

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

Redis系列--布隆过滤器(Bloom Filter)_第2张图片

1、添加key

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

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

Redis系列--布隆过滤器(Bloom Filter)_第3张图片

 2、查询key

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

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

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

Redis系列--布隆过滤器(Bloom Filter)_第4张图片

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

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

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

Redis系列--布隆过滤器(Bloom Filter)_第5张图片

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

Redis系列--布隆过滤器(Bloom Filter)_第6张图片

1、hash函数 

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

Redis系列--布隆过滤器(Bloom Filter)_第7张图片

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

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

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

这种情况称为“散列碰撞(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 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数据库

Redis系列--布隆过滤器(Bloom Filter)_第8张图片

四、布隆过滤器优缺点 

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

缺点:

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 bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE, FPP);

        //1 先往布隆过滤器里面插入100万的样本数据
        for (int i = 1; i <= SIZE; i++) {
            bloomFilter.put(i);
        }
        //故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里
        List 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

Redis系列--布隆过滤器(Bloom Filter)_第9张图片

BloomFilter 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 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 {

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

    /**
     * @param expectedInsertions 预估插入数据数量
     * @param fpp                允许数据误差率
     * @param funnel
     */
    public BloomFilterHelper(Long expectedInsertions, double fpp, Funnel 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 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("/**");
    }

}

四、总结

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

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

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内存也会飙升。在这个情况下可以考虑一下布隆过滤器的使用了,加一个黑名单操作。

你可能感兴趣的:(Redis,redis,数据库,缓存)