redis缓存穿透解决之布隆过滤器

本文介绍了布隆过滤器的原理与实现,包括Google Guava库中的布隆过滤器使用,以及如何利用Redis内置的布隆过滤器解决缓存穿透问题。通过示例代码展示了在秒杀场景中,如何利用布隆过滤器预校验数据,减少无效数据库查询,同时探讨了误判率和内存占用的平衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、原理实现

二、Google工具包Guava实现布隆过滤器

三、Redis布隆过滤器解决缓存穿透

四、安装rebloom


想要尽量避免缓存穿透,一个办法就是对数据进行预校验,在对Redis和数据库进行操作前,先检查数据是否存在,如果不存在就直接返回。如果我们想要查询一个元素是否存在,要保证查询效率,可以选择HashSet,但是如果有10亿个数据,都用HashSet进行存储,内存肯定是无法容纳的。这时就需要布隆过滤器了

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

特点:

  • 高效地插入和查询,占用空间少,返回的结果是不确定性的。
  • 一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。
  • 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。
  • 误判只会发生在过滤器没有添加过的元素,对于添加过的元素不会发生误判。

一、原理实现

① 初始化

 布隆过滤器 本质上 是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0在这里插入图片描述
② 添加

当我们向布隆过滤器中添加数据时,为了尽量地址不冲突,会使用多个 hash 函数对 key 进行运算,算得一个下标索引值,然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。

例如,我们添加一个字符串wmyskxz


③ 查询

向布隆过滤器查询某个key是否存在时,先把这个 key 通过相同的多个 hash 函数进行运算,查看对应的位置是否都为 1,

只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在;

如果这几个位置全都是 1,那么说明极有可能存在;

为什么说极有可能呢?

因为这些位置的 1 可能是因为其他的 key 存在导致的,也就是hash冲突

就比如我们在 add 了字符串wmyskxz数据之后,很明显1/3/5 这几个位置的 1 ,如上图,是因为第一次添加的 wmyskxz 而导致的;此时我们查询一个没添加过的不存在的字符串inexistent-key,它有可能计算后坑位也是1/3/5 ,这时候经过hash函数计算以及查询发现1 /3 / 5 的位置都是1,所以布隆过滤器认为该数据是存在的,就发生了误判
在这里插入图片描述为什么key不能删除呢?

因为该key经过运算放入bit数组中的值,并不一定是你产生的,有可能其他数值也共用着这个数位,如果你删除了该位置,别的key进行查询时可能导致查询失败

二、Google工具包Guava实现布隆过滤器

  • 首先将maven中导入相关依赖(其他一些SpringBoot整合redis依赖就不赘述了)
<!--Google 开源的 Guava 中自带的布隆过滤器-->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>
  • 模拟秒杀场景获取商品详情页面的单机版Guava实现布隆过滤器

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import lombok.Data;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * @Description:模拟秒杀,演示布隆过滤器业务逻辑比较简单
 * @Date 2021/6/20 17:26
 * @@author: A.iguodala
 */
@RestController
public class SeckillGuavaBloomFilterDemoController implements InitializingBean {

    /**
     * 模拟秒杀服务,真实中用自动注入
     * @Autowired
     */
    private SeckillService seckillService = new SeckillService();

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 布隆过滤器
     * 三个参数 (Funnel<? super T> funnel, long expectedInsertions, double fpp)
     * Funnel<? super T> funnel : 数据类型 (long 保存商品ID)
     * long expectedInsertions : 希望插入值的个数
     * double fpp : 错误率,不设置默认为0.03 ,过小的话误判太多,造成数据库接受到大量请求,过大数组长度过长效率降低
     */
    BloomFilter<Long> filter = BloomFilter.create(Funnels.longFunnel(), 100,0.03);

    /**
     * 模拟获取商品详情请求
     * @param goodsId
     * @return
     */
    @RequestMapping(value = "/toDetail/{goodsId}")
    public Map<String, Object> toDetail(@PathVariable(value = "goodsId") Long goodsId) {
        Map<String, Object> map = new HashMap<>();

		/**
         * 先判断该商品是存在,存在则查询缓存或者数据库,不存在直接返回
         */
        if (filter.mightContain(goodsId)) {
            SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.opsForValue().get("goods:" + goodsId);
            if (seckillGoods == null) {
                seckillGoods = seckillService.get();
                redisTemplate.opsForValue().set("goods:" + goodsId, seckillGoods);
            }
            map.put("data", seckillGoods);
            map.put("code", 200);
            return map;
        }else {
            map.put("code", 400);
            return map;
        }
    }

    /**
     * 缓存预热,将秒杀商品加入到redis以及boolmFilter中
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {

        List<SeckillGoods> goodsList = seckillService.list();
        if (CollectionUtils.isEmpty(goodsList)) {
            return;
        }
        // 循环将数据加入过滤器
        goodsList.forEach(goods -> {
            filter.put(goods.getId());
        });
    }
}

/**
 * 模拟秒杀服务
 */
class SeckillService {

    /**
     * 模拟查询所有商品
     * @return
     */
    List<SeckillGoods> list() {
        return null;
    }

    /**
     * 模拟查询商品
     * @return
     */
    SeckillGoods get() {
        return null;
    }
}

/**
 * 模拟秒杀商品
 */
@Data
class SeckillGoods {

    /**
     * 商品ID
     */
    private Long id;
}

三、Redis布隆过滤器解决缓存穿透

  • 导入 redisson 依赖,里面包括布隆过滤器,分布式锁等
<!-- redisson -->
<groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>
  • 和上一个同样的场景,使用redis自己的布隆过滤器,区别在于该过滤器可以使用集群的方式
  • config.useClusterServers().addNodeAddress(“redis://127.0.0.1:6379”, “redis://127.0.0.2:6379”);

import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.redisson.config.Config;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Description:模拟秒杀获取商品详情功能
 * @Date 2021/6/20 22:27
 * @@author: A.iguodala
 */
@RestController
public class SeckillRedissonBloomFilterDemoController implements InitializingBean {

    /**
     * 操作redisson客户端(jedis)
     */
    static RedissonClient redissonClient = null;

    /**
     * redis版内置的布隆过滤器
     */
    static RBloomFilter rBloomFilter = null;

    /**
     * 模拟秒杀服务,真实中用自动注入
     * @Autowired
     */
    private SeckillService seckillService = new SeckillService();
    
    @Autowired
    private RedisTemplate redisTemplate;

    static
    {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
        
        // 集群版
        //config.useClusterServers().addNodeAddress("redis://127.0.0.1:6379", "redis://127.0.0.1:6379");
        
        //构造redisson
        redissonClient = Redisson.create(config);
        //通过redisson构造rBloomFilter
        rBloomFilter = redissonClient.getBloomFilter("seckillGoodsBloomFilter",new StringCodec());
        rBloomFilter.tryInit(10000,0.03);
    }

    /**
     * 模拟获取商品详情请求
     * @param goodsId
     * @return
     */
    @RequestMapping(value = "/toDetail/{goodsId}")
    public Map<String, Object> toDetail(@PathVariable(value = "goodsId") Long goodsId) {
        Map<String, Object> map = new HashMap<>();
        /**
         * 先判断该商品是存在,存在则查询缓存或者数据库,不存在直接返回
         */
        if (rBloomFilter.contains(goodsId)) {
            SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.opsForValue().get("goods:" + goodsId);
            if (seckillGoods == null) {
                seckillGoods = seckillService.get();
                redisTemplate.opsForValue().set("goods:" + goodsId, seckillGoods);
            }
            map.put("data", seckillGoods);
            map.put("code", 200);
            return map;
        }else {
            map.put("code", 400);
            return map;
        }
    }
    /**
     * 缓存预热,将秒杀商品加入boolmFilter中
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {

        List<SeckillGoods> goodsList = seckillService.list();
        if (CollectionUtils.isEmpty(goodsList)) {
            return;
        }
        // 循环将数据加入过滤器
        goodsList.forEach(goods -> {
            rBloomFilter.add(goods.getId());
        });
    }
}

/**
 * 模拟秒杀服务
 */
class SeckillService {

    /**
     * 模拟查询所有商品
     * @return
     */
    List<SeckillGoods> list() {
        return null;
    }

    /**
     * 模拟查询商品
     * @return
     */
    SeckillGoods get() {
        return null;
    }
}

/**
 * 模拟秒杀商品
 */
@Data
class SeckillGoods {

    /**
     * 商品ID
     */
    private Long id;
}

四、安装rebloom

安装一个redis的插件rebloom,可以通过命令行来对redis操作布隆过滤器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值