Spring Boot集成Redisson布隆过滤器案例

1 什么是布隆过滤器

布隆过滤器实际上是一个非常长的二进制向量(bitmap)和一系列随机哈希函数。

那什么又叫哈希函数呢?

哈希函数指将哈希表中元素的关键键值通过一定的函数关系映射为元素存储位置的函数。(HashMap源码)

 布隆过滤器的优点:

  • 布隆过滤器存储空间和插入/查询时间都是常数
  • Hash函数相互之间没有关系,方便由硬件并行实现
  • 布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势
  • 布隆过滤器可以表示全集,其它任何数据结构都不能

  布隆过滤器的缺点:

  • 有一定的误判率(常见的弥补措施是:建立一个小的白名单,存储那些可能被误判的元素。但是如果元素数量太少,使用散列表足矣。)
  • 一般情况下不能从布隆过滤器中删除元素。()

2 布隆过滤器的作用

布隆过滤器可以用于检索一个元素是否在一个集合中,常用于解决如下问题:

  • 解决Redis缓存穿透
  • 邮件过滤,使用布隆过滤器来做邮件黑名单过滤
  • 解决视频推荐过的不再推荐

3 布隆过滤器的基本原理

  • 首先,建立一个二进制向量,并将所有位设置为0。
  • 然后,选定K个散列函数,用于对元素进行K次散列,计算向量的位下标。
  • 添加元素:当添加一个元素到集合中时,通过K个散列函数分别作用于元素,生成K个值作为下标,并将向量的相应位设置为1。
  • 检查元素:如果要检查一个元素是否存在集合中,用同样的散列方法,生成K个下标,并检查向量的相应位是否全部是1。如果全为1,则该元素很可能在集合中;否则(只要有1个或以上的位为0),该元素肯定不在集合中。

在这里插入图片描述

4 在Spring Boot中集成Redisson实现布隆过滤器 

在这里插入图片描述

 4.1 添加maven依赖

不再需要spring-boot-starter-data-redis依赖,但是都添加也不会报错

<!--redisson-->
<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson-spring-boot-starter</artifactId>
	<version>3.17.0</version>
</dependency>

 4.2 配置yml

spring:
  datasource:
    username: xx
    password: xxxxxx
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=CTT
  cache:
    type: redis
  redis:
    database: 0
    port: 6379               # Redis服务器连接端口
    host: localhost          # Redis服务器地址
    password: xxxxxx         # Redis服务器连接密码(默认为空)
    timeout: 5000            # 超时时间

 4.3 配置RedissonConfig

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.Random;

@EnableCaching
@Configuration
public class RedissonConfig {

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

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

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


    @Bean(destroyMethod = "shutdown")  // bean销毁时关闭Redisson实例,但不关闭Redis服务
    public RedissonClient redisson() {
        //创建配置
        Config config = new Config();
        /**
         *  连接哨兵:config.useSentinelServers().setMasterName("myMaster").addSentinelAddress()
         *  连接集群: config.useClusterServers().addNodeAddress()
         */
        config.useSingleServer()
                .setAddress("redis://" + host + ":" + port)
                .setPassword(password)
                .setTimeout(5000);
        //根据config创建出RedissonClient实例
        return Redisson.create(config);
    }

    @Bean
    public CacheManager RedisCacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        /**
         * 新版本中om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL)已经被废弃
         * 建议替换为om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL)
         */
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化解决乱码的问题
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 设置缓存过期时间  为解决缓存雪崩,所以将过期时间加随机值
                .entryTtl(Duration.ofSeconds(60 * 60 + new Random().nextInt(60 * 10)))
                // 设置key的序列化方式
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                // 设置value的序列化方式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        // .disableCachingNullValues(); //为防止缓存击穿,所以允许缓存null值
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                // 启用RedisCache以将缓存 put/evict 操作与正在进行的 Spring 管理的事务同步
                .transactionAware()
                .build();
        return cacheManager;
    }
}

4.4 工具类BloomFilterUtil

import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class BloomFilterUtil {

    @Resource
    private RedissonClient redissonClient;

    /**
     * 创建布隆过滤器
     *
     * @param filterName         过滤器名称
     * @param expectedInsertions 预测插入数量
     * @param falsePositiveRate  误判率
     */
    public <T> RBloomFilter<T> create(String filterName, long expectedInsertions, double falsePositiveRate) {
        RBloomFilter<T> bloomFilter = redissonClient.getBloomFilter(filterName);
        bloomFilter.tryInit(expectedInsertions, falsePositiveRate);
        return bloomFilter;
    }
}

 4.5 编写service实现层

 其它层正常编写即可,与之前并无差

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.company.springboot.entity.User;
import com.company.springboot.mapper.UserMapper;
import com.company.springboot.service.UserService;
import com.company.springboot.util.BloomFilterUtil;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {
    // 预期插入数量
    static long expectedInsertions = 200L;
    // 误判率
    static double falseProbability = 0.01;

    // 非法请求所返回的JSON
    static String illegalJson = "[\"com.company.springboot.entity.User\",{\"id\":null,\"userName\":\"null\",\"sex\":null,\"age\":null}]";

    private RBloomFilter<Long> bloomFilter = null;

    @Resource
    private BloomFilterUtil bloomFilterUtil;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private UserMapper userMapper;

    @PostConstruct // 项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法
    public void init() {
        // 启动项目时初始化bloomFilter
        List<User> userList = this.list();
        bloomFilter = bloomFilterUtil.create("idWhiteList", expectedInsertions, falseProbability);
        for (User user : userList) {
            bloomFilter.add(user.getId());
        }
    }


    @Cacheable(cacheNames = "user", key = "#id", unless = "#result==null")
    public User findById(Long id) {
        // bloomFilter中不存在该key,为非法访问
        if (!bloomFilter.contains(id)) {
            System.out.println("所要查询的数据既不在缓存中,也不在数据库中,为非法key");
               /**
             * 设置unless = "#result==null"并在非法访问的时候返回null的目的是不将该次查询返回的null使用
             * RedissonConfig-->RedisCacheManager-->RedisCacheConfiguration-->entryTtl设置的过期时间存入缓存。
             *
             * 因为那段时间太长了,在那段时间内可能该非法key又添加到bloomFilter,比如之前不存在id为1234567的用户,
             * 在那段时间可能刚好id为1234567的用户完成注册,使该key成为合法key。
             *
             * 所以我们需要在缓存中添加一个可容忍的短期过期的null或者是其它自定义的值,使得短时间内直接读取缓存中的该值。
             *
             * 因为Spring Cache本身无法缓存null,因此选择设置为一个其中所有值均为null的JSON,
             */
            redissonClient.getBucket("user::" + id, new StringCodec()).set(illegalJson, new Random().nextInt(200) + 300, TimeUnit.SECONDS);
            return null;
        }
        // 不是非法访问,可以访问数据库
        System.out.println("数据库中得到数据*****");
        return userMapper.selectById(id);
    }

    // 先执行方法体中的代码,成功执行之后删除缓存
    @CacheEvict(cacheNames = "user", key = "#id")
    public boolean delete(Long id) {
        // 删除数据库中具有的数据,就算此key从此之后不再出现,也不能从布隆过滤器删除
        return userMapper.deleteById(id) == 1;
    }

    // 如果缓存中先前存在,则更新缓存;如果不存在,则将方法的返回值存入缓存
    @CachePut(cacheNames = "user", key = "#user.id")
    public User update(User user) {
        userMapper.updateById(user);
        // 新生成key的加入布隆过滤器,此key从此合法,因为该更新方法并不更新id,所以也不会产生新的合法的key
        bloomFilter.add(user.getId());
        return user;
    }

    @CachePut(cacheNames = "user", key = "#user.id")
    public User insert(User user) {
        userMapper.insert(user);
        // 新生成key的加入布隆过滤器,此key从此合法
        bloomFilter.add(user.getId());
        return user;
    }
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
概要介绍: 本课程主要是介绍并实战一款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进阶实战者
Sure!以下是一个示例代码,展示了如何在Java Spring Boot中使用布隆过滤器Redis来解决缓存穿透问题: 首先,你需要在pom.xml文件中添加相应的依赖: ```xml <dependencies> <!-- Spring Boot Starter Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Guava Bloom Filter --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1-jre</version> </dependency> </dependencies> ``` 接下来,创建一个布隆过滤器的工具类 BloomFilterUtil.java: ```java import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; @Component public class BloomFilterUtil { @Autowired private RedisTemplate<String, Object> redisTemplate; private BloomFilter<String> bloomFilter; // 设置布隆过滤器的预计插入数据量和误判率 private static final int EXPECTED_INSERTIONS = 1000000; private static final double FPP = 0.001; @PostConstruct public void init() { // 创建布隆过滤器,并将其保存到Redis中 bloomFilter = BloomFilter.create(Funnels.stringFunnel(), EXPECTED_INSERTIONS, FPP); redisTemplate.opsForValue().set("bloomFilter", bloomFilter); } public boolean mightContain(String key) { // 从Redis中获取布隆过滤器 bloomFilter = (BloomFilter<String>) redisTemplate.opsForValue().get("bloomFilter"); // 使用布隆过滤器判断key是否可能存在 return bloomFilter.mightContain(key); } public void put(String key) { // 从Redis中获取布隆过滤器 bloomFilter = (BloomFilter<String>) redisTemplate.opsForValue().get("bloomFilter"); // 将key添加到布隆过滤器中 bloomFilter.put(key); // 将更新后的布隆过滤器保存到RedisredisTemplate.opsForValue().set("bloomFilter", bloomFilter); } } ``` 然后,在你需要使用布隆过滤器解决缓存穿透的地方,注入 BloomFilterUtil,并使用它来判断数据是否存在于缓存中: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class CacheController { @Autowired private BloomFilterUtil bloomFilterUtil; @GetMapping("/data/{key}") public String getData(@PathVariable String key) { // 先使用布隆过滤器判断key是否可能存在于缓存中 if (bloomFilterUtil.mightContain(key)) { // 如果可能存在,再从缓存中获取数据 String data = redisTemplate.opsForValue().get(key); if (data != null) { return data; } } // 如果数据不在缓存中,进行其他操作(例如从数据库中查询数据) // ... return null; } } ``` 这样,当有大量的请求同时访问某个缓存时,在经过布隆过滤器的判断后,可以避免无效的缓存查询请求,减轻了数据库的负载压力。 请注意,以上代码只是示例,实际使用时需要根据具体的业务需求进行适当的修改和优化。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值