springboot使用布隆过滤器——缓存穿透

目录

1.布隆过滤器原理

2.具体使用场景

3.springboot集成布隆过滤器

4.总结

1.布隆过滤器原理

布隆过滤器(Bloom Filter)是非常经典的以空间换时间的算法。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

布隆过滤器一般常用于存储数据,并判断数据是否存储与过滤器中。他的原理如下:

当一个数据存入过滤器中时,会经过哈希函数进行计算(图中是三个,不一定是三个),计算后得出对应的结果,存入对应的位中,将其值改为1。‘

当查询一个值时,会经过同样的哈希运算,然后去找寻对应的位值,如果都为1,即判断该值可能会存在;若有一个值不为1,都认为该值不存在于过滤器中。

但布隆过滤器会存在误判;

如上图所示,两个不同的值,经过相同的哈希运算后,可能会得出同样的值。即上图中,hello和你好经过哈希运算后,都为2,把位2上的值改为1。所以,无法判断位2上的值为1是谁的值。同时,如果只存储了"你好"未存储"hello",当查询hello时,经过哈希运算得出值为2,去位2中查看,得知值为1,得出结论"hello"可能存在于过滤器中,即发生了误判。

误判可以通过增多哈希函数进行降低。哈希函数越多,误判率越低。同时,布隆过滤器查找和插入的时间复杂度都为O(n),n为哈希函数的个数。所以,哈希函数越多,时间复杂度越高。具体如何选择,需要根据数据量的多少进行。

布隆过滤器优缺点如下:

  • 相比于其它的数据结构,由于布隆过滤器不存储数据本身,而使用二进制位来存储数据,在空间和时间方面都有巨大的优势,且在某些对保密要求非常严格的场合有优势。
  • 由于上文中提到的数据经过哈希计算后值相同的原因,一般情况下不能从布隆过滤器中删除元素。

2.具体使用场景

布隆过滤器可以应用于缓存穿透场景中。缓存穿透,一般判断数据是否在缓存中,如果在则直接返回结果,不在则查询数据库;如果来一波冷数据(比如使用大量随机生成的uuid进行查询),由于这个key不存在于redis中,会导致缓存大量击穿,于是服务器会去请求mysql,但是在mysql中也找不到相应的记录。此时请求全都打在了mysql上,导致数据库压力剧增,甚至可能崩溃。该问题可以通过在redis存储null值解决。但当无用请求大量增多进行攻击时,会知道redis缓存中存储大量的null值数据,出现另外的问题。

这时候可以用布隆过滤器对请求进行过滤。只有在布隆过滤器中,才去查询数据库。如果不在布隆器中,则直接返回。避免了数据库的压力。

我们可以设计一个场景方案,如下:

请求进来后先在过滤器中进行查询,如果布隆过滤器判断编号可能存在,则直接去读取存储在 Redis 缓存中的数据;如果此时 Redis 缓存没有存在对应的商品数据,则直接去读取数据库,并将读取到的信息重新载入到 Redis 缓存中。如果布隆过滤器判断编号不存在,直接过滤该请求。

3.springboot集成布隆过滤器

springboot中一般可以通过两种方式调用布隆过滤器(主要演示基于redission实现的布隆过滤器整套业务):

(1)通过redission实现(需要先使用redis下载布隆过滤器插件):

        <!--redission相关依赖-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.17.7</version>
        </dependency>

配置文件:

import org.redisson.Redisson;
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;

@Configuration
public class RedissonConfig {

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

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

    @Bean
    public RedissonClient getRedisson(){

        Config config = new Config();
        //单机模式  依次设置redis地址和密码
        config.useSingleServer().
                setAddress("redis://" + host + ":" + port);
        return Redisson.create(config);
    }
}

 业务类(下面代码中整合了mybatis-plus和redission实现分布式锁下的数据查询,不熟悉可以不用修改为简单的数据库查询):

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.seven.springcloud.Pojo.Address;
import com.seven.springcloud.dao.UserAddressMapper;
import com.seven.springcloud.service.UserAddressService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

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

@Service
@Slf4j
public class UserAddressServiceImpl extends ServiceImpl<UserAddressMapper, Address> implements UserAddressService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private UserAddressMapper userAddressMapper;
    @Resource
    private RedissonClient redisson;

    //基于redission分布式锁下的mybatis-plus数据库查询
        public Map<String,Address> searchByDb(int id){

        Map<String,Address> map = new HashMap<>();
        RLock lock = redisson.getFairLock("myLock");       //获取锁
        try {
            lock.lock();    //上锁
            synchronized (this){
                if(StringUtils.isEmpty(stringRedisTemplate.opsForValue().get("addressList"))){
                    log.info("查数据库");
                    Address address = userAddressMapper.selectById(id);
                    map.put("placeList",address);
                }else {
                    log.info("缓存击中!");
                    return JSON.parseObject(stringRedisTemplate.opsForValue().get("addressList"),new TypeReference<Map<String,List<Address>>>(){});
                }
            }
        }catch (Exception e){
            log.warn("系统错误,稍后重试");
        }
        finally {
            lock.unlock();    //删除锁
        }

        return map;
    }

    //添加数据
    public int bloomAdd(Address address) {
        //数据库中插入数据
        int result = userAddressMapper.insert(address);
        //插入成功,存入布隆过滤器中
        if(result > 0){
            //获取布隆过滤器
            RBloomFilter<Object> bloomFilter = redisson.getBloomFilter("idList");
            //初始化布隆过滤器(数据量,误差率)
            bloomFilter.tryInit(1000000L,0.02);
            //往过滤器中加入数据
            bloomFilter.add(address.getId());
        }
        return result;
    }

    //查询
    public Map<String,Address> bloomFilter(int id) {

        //获取布隆过滤器
        RBloomFilter<Object> bloomFilter = redisson.getBloomFilter("idList");
        //判断数据是否在过滤器中
        boolean flag = bloomFilter.contains(id);
        if(flag){
            //存在,查缓存
            String addressList = stringRedisTemplate.opsForValue().get("addressList");
            //判断缓存中是否存在
            if (StringUtils.isEmpty(addressList)){
                log.info("缓存未命中");
                //调用查询数据库的方法
                Map<String,Address> map = searchByDb(id);
                //封装查询结果
                String result = JSON.toJSONString(map);
                //以json格式存入redis中
                stringRedisTemplate.opsForValue().set("addressList",result);
                return map;
            }else {
                log.info("缓存命中");
                //直接返回缓存数据
                return JSON.parseObject(addressList,new TypeReference<Map<String,Address>>(){});
            }
        }else {
            //过滤器不命中,过滤请求、
            log.info("请求被过滤");
            return null;
        }
    }

}

如上所示:在插入数据时,将实体类的唯一id作为布隆过滤器值插入过滤器中,用以判断请求。

当用户发来请求时,会附加一个编号,如果布隆过滤器判断编号存在,则直接去读取存储在 Redis 缓存中的数据;如果此时 Redis 缓存没有存在对应的商品数据,则直接去读取数据库,并将读取到的信息重新载入到 Redis 缓存中。这样下一次用户在查询相同编号数据时,就可以直接读取缓存了

 我们打开redis,可以看到如下key:

其中idList是布隆过滤器存储的id编号;addressList是redis缓存的数据值;config 是布隆过滤器的相关依赖。

具体业务过程中,实体类的key值还需要有一定的区别,不该使用统一的String值(如上述代码中的addressList,主要是为了方便演示),可以使用字符+编号的形式,比如 Address:{id},还需自行进行设计。

(2)使用GUAVA实现布隆过滤器

        <!--布隆过滤器所需依赖-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>30.0-jre</version>
        </dependency>

 下面是一个简单使用,可进行参考:

/**
 * Guava版布隆过滤器
 *
 */
public class BloomFilterTest {

    /**
     * @param expectedInsertions 预期插入值
     *  这个值的设置相当重要,如果设置的过小很容易导致饱和而导致误报率急剧上升,如果设置的过大,也会对内存造成浪费,所以要根据实际情况来定
     * @param fpp                误差率,例如:0.001,表示误差率为0.1%
     * @return 返回true,表示可能存在,返回false一定不存在
     */
    public static boolean isExist(int expectedInsertions, double fpp) {
        // 创建布隆过滤器对象
        BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 500, 0.01);

        // 判断指定元素是否存在
        System.out.println(filter.mightContain(10));

        // 将元素添加进布隆过滤器
        filter.put(10);

        // 再判断指定元素是否存在
        System.out.println(filter.mightContain(10));
        return filter.mightContain(10);
    }

    //主类中进行测试
    public static void main(String[] args) {

        boolean exist = isExist(100000000, 0.001);
    }
}

4.总结

布隆过滤器主要就是利用一个很长的二进制数组,通过一系列的hash函数来确定该数据是否存在。可以避免由于恶意用户在短时内大量查询不存在的数据,导致大量请求被送达数据库进行查询,当请求数量超过数据库负载上限时,使系统响应出现高延迟甚至瘫痪的问题。

针对布隆过滤器误判的问题,其实在大多数情况下,我们出现误判也不会对系统产生额外的影响。因为像刚才我们设置 0,02 的误判率,1 万次请求才可能会出现 200 次误判的情况。我们已经将大多数的无效请求进行了拦截。 所以我们不需要设置过低的误判率,而需要根据业务需求动态进行设置,避免执行时间过长。

针对删除困难问题,我们可以mybatis-plus中的逻辑删除。即不从表中删除某条记录,而是增加一个状态字段,将这行记录的状态字段设为已删除状态。这样可以一定程度上解决布隆过滤器的删除问题,也不会对其性能造成太大影响。如果追求更好的性能,可以使用布谷鸟过滤器,具体不在此处阐释。

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
概要介绍: 本课程主要是介绍并实战一款java中间件~redisson,介绍redisson相关的核心技术栈及其典型的应用场景,其中的应用场景就包括布隆过滤器、限流器、短信发送、实时/定时邮件发送、数据字典、分布式服务调度等等,在业界号称是在java项目里正确使用redis的姿势。本课程的目标就在于带领各位小伙伴一起学习、攻克redisson,更好地巩固自己的核心竞争力,而至于跳槽涨薪,自然不在话下!  课程内容: 说起redisson,可能大伙儿不是很熟悉,但如果说起redis,想必肯定很多人都晓得。没错,这家伙字如其名,它就是架设在redis基础上的一款综合性的、新型的中间件,号称是java企业级应用开发中正确使用redis的姿势/客户端实例。 它是架设在redis基础之上,但拥有的功能却远远多于原生Redis 所提供的,比如分布式对象、分布式集合体系、分布式锁以及分布式服务调度等一系列具有分布式特性的对象实例… 而这些东西debug将在本门课程进行淋漓尽致的介绍并实战,除此之外,我们将基于spring boot2.0搭建的多模块项目实战典型的应用场景:对象存储、数据字典、短信发送、实时/定时邮件发送、布隆过滤器、限流组件、分布式服务调度....课程大纲如下所示: 下面罗列一下比较典型的核心技术栈及其实际业务场景的实战,如下图所示为redisson基于订阅-发布模式的核心技术~主题Topic的实际业务场景,即实时发送邮件: 而下图则是基于“多值映射MultiMap”数据结构实战实现的关于“数据字典”的缓存管理: 除此之外,我们还讲解了可以与分布式服务调度中间件dubbo相媲美的功能:分布式远程服务调度,在课程中我们动手搭建了两个项目,用于分别充当“生产者”与“消费者”角色,最终通过redisson的“服务调度组件”实现服务与服务之间、接口与接口之间的调用!  课程收益: (1)认识并掌握redisson为何物、常见的几种典型数据结构-分布式对象、集合、服务的应用及其典型应用场景的实战; (2)掌握如何基于spring boot2.0整合redisson搭建企业级多模块项目,并以此为奠基,实战企业级应用系统中常见的业务场景,巩固相应的技术栈! (3)站在项目管理与技术精进的角度,掌握对于给定的功能模块进行业务流程图的绘制、分析、模块划分、代码实战与性能测试和改进,提高编码能力与其他软实力; (4)对于Java微服务、分布式、springboot精进者而言,学完本课程,不仅可以巩固提高中间件的实战能力,其典型的应用场景更有助于面试、助力相关知识点的扫盲! 如下图所示: 关键字:Spring BootRedis,缓存穿透,缓存击穿,缓存雪崩,红包系统,Mybatis,高并发,多线程并发编程,发送邮件,列表List,集合Set,排行榜,有序集合SortedSet,哈希Hash ,进阶实战,面试,微服务、分布式 适用人群:redisson学习者,分布式中间件实战者,微服务学习者,java学习者,spring boot进阶实战者,redis进阶实战者

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值