布隆过滤器在项目中的使用(spring AOP+自定义注解+spring 定时器)

布隆过滤器在项目中的使用

    • 概念
    • 使用介绍
    • 使用步骤
      • 1. 添加依赖
      • 2. 编写对应的布隆过滤器扫描器
      • 3. 编写刷新布隆过滤器数据定时器

概念

Redisson 的「布隆过滤器」需要将当前的元素经过事先设计构建好的 K 个哈希函数计算出 K 个哈希值,并将预先已经构建好的「位数组」的相关下标取值置为 1 。当某个元素需要判断是否已存在时,则同样是先经过 K 个哈希函数求取 K 个哈希值,并判断「位数组」相应的 K 个下标的取值是否都为 1 。如果是,则代表元素是「大概率」是存在的;否则,表示该元素一定不存在。

使用介绍

由于项目中需要将查找的数据进行布隆过滤器进行过滤,使用的原因如下:

由于前端会向后台请求数据,数据库中不存在该数据,会先向对应的redis中查询,如果redis中没有该数据则会向数据库去查询,当数据库没有该数据,那么多次请求后会损耗数据库的性能,

解决方案:
在redis中存储查询的空数据返回给前端

后续问题:
如果前端随机id进行查询的话,redis可能存储过多的无用数据占用内存

这个时候就需要在redis查询之前做一个布隆过滤器进行数据判断

项目上使用思路:

启动时思路:定义一个需要创建布隆过滤器的注解,将注解标注到mapper头上,通过实现ApplicationListener接口扫描整个项目上被标注的该注解的类,由于本项目使用的mybatis plus,则将注解标注到mapper类上,通过反射调用selectList方法获取对应的数据集合,再通过反射获取baseMapper接口上的泛型参数,再通过泛型参数获取getId方法,将id集合存储到布隆过滤其中;

更新思路 :由于布隆过滤器数据不能删除,则在晚上再次扫描一次注解,重新生成新的布隆过滤器,保证数据的准确性;

使用步骤

1. 添加依赖


        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
            <exclusions> 
                <exclusion>
                    <groupId>io.lettucegroupId>
                    <artifactId>lettuce-coreartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
        dependency>
        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.15.6version>
        dependency>

2. 编写对应的布隆过滤器扫描器

使用spring框架时继承ApplicationListenter 后会在项目启动后运行这个方法从起到扫描的作用

@Component
@Slf4j
public class InjectionScan implements ApplicationListener<ContextRefreshedEvent> {

    @Resource
    private AuthServiceClient authServiceClient;

    @Resource
    private RedissonClient redissonClient;

    @Value("${spring.application.name}")
    private String serviceName;

    @Resource
    private ApplicationContext applicationContext;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
//        generatePermission(event);
        generateBloomFilterKey(event);
    }

    public void generateBloomFilterKey(ContextRefreshedEvent event){
        Long bloomNum = 100_000L;
        Map<String, Object> beansWithAnnotation = event.getApplicationContext().getBeansWithAnnotation(BloomFilterScan.class);
        for (String s : beansWithAnnotation.keySet()) {
            Object controller = beansWithAnnotation.get(s);
            //获取代理类对象对象
            Class<?> aClass = controller.getClass();
            //获取接口对象
            Class<?> anInterface= aClass.getInterfaces()[0];
            //从ioc容器中获取mysqlDao对象
            Object mysqlDao = applicationContext.getBean(anInterface);
            //获取baseMapper的泛型类型
            Class entityClass = null;
            Method getId = null;
            try {
                ParameterizedType parameterizedType = (ParameterizedType) anInterface.getGenericInterfaces()[0];
                entityClass = (Class) parameterizedType.getActualTypeArguments()[0];
                //找到对应getId方法
                getId = Arrays.stream(entityClass.getDeclaredMethods()).filter(method -> Objects.equals("getId", method.getName())).collect(Collectors.toList()).get(0);
            } catch (Exception e) {
                e.printStackTrace();
                //如果找不到方法,就跳出该循环
                continue;

            }
            //存储获取的id集合
            List<Object> idList = new ArrayList<>();
            for (Method declaredMethod : aClass.getDeclaredMethods()) {
                if ("selectList".equals(declaredMethod.getName())){
                    try {
                        //获取数量的集合
                        List<Object> objectList = (List<Object>) declaredMethod.invoke(mysqlDao, new QueryWrapper<>());
                        //判断获取的集合数量是否大于设定的布隆过滤器数据量大小
                        if (objectList.size()>bloomNum){
                            //大于的话就扩大10倍
                            bloomNum = bloomNum*10L;
                        }
                        for (Object po : objectList) {
                            //运行getId方法获取对应id值
                            Object invoke = getId.invoke(po);
                            //存入到对应集合中
                            idList.add(invoke);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            //设置布隆过滤器的key,目前为po类的名字
            RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(entityClass.getSimpleName());
            //给当前布隆过滤器的设置一个0秒的过期时间
            bloomFilter.expire(0, TimeUnit.SECONDS);
            //清除过去数据
            bloomFilter.clearExpire();
            //创建一个5%误差率,数量为自定义变量的空间
            bloomFilter.tryInit(bloomNum,0.005);
            //循环遍历id并存入到布隆过滤器中
            for (Object id : idList) {
                bloomFilter.add(id);
            }
            log.info("表 "+entityClass.getSimpleName()+" 数据的id已存入布隆过滤器中");

        }
    }


    //扫描controller类生产权限表
    private void generatePermission(ContextRefreshedEvent event) {
        Map<String, Object> beansWithAnnotation = event.getApplicationContext().getBeansWithAnnotation(RestController.class);
        for (String s : beansWithAnnotation.keySet()) {
            Object o = beansWithAnnotation.get(s);
            String path1 = "";
            String path2 = "";
            String methodType = "";
            String roleName = "";
            String rightsName = "";
            String describe = "";
            Class<?> aClass = o.getClass();
            path1 = aClass.getAnnotation(RequestMapping.class).value()[0];
            for (Method method : aClass.getDeclaredMethods()) {
                if (method.getAnnotation(PermissionInjection.class) != null) {
                    PermissionInjection permissionInjection = method.getAnnotation(PermissionInjection.class);
                    GetMapping getMapping = method.getAnnotation(GetMapping.class);
                    PostMapping postMapping = method.getAnnotation(PostMapping.class);
                    RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                    if (getMapping != null) {
                        path2 = getMapping.value()[0];
                        methodType = "GET";
                    } else if (postMapping != null) {
                        path2 = postMapping.value()[0];
                        methodType = "POST";
                    } else if (requestMapping != null) {
                        path2 = requestMapping.value()[0];
                    }
                    rightsName = permissionInjection.rightsName();
                    roleName = permissionInjection.roleName();
                    describe = permissionInjection.describer();
                    //如果第一个路径不为空则加斜杠
//                        if (!"".equals(path1)) path1 = path1 +"/";
                    //拼接权限信息
                    String authority = roleName;
                    if (!"".equals(rightsName)) authority = roleName + "," + rightsName;
                    //拼接路径信息
                    String uri = "/" + serviceName + path1 + path2;
                    String machiningUri = machiningUri(uri);
                    System.out.println("当前路径为 " + machiningUri + "\t角色名为 " + roleName + "\t权限名为 " + rightsName + "\t描述" + describe);
                    authServiceClient.deleteUriByUri(machiningUri);
                    authServiceClient.addUri(new ServiceUriAuthorityDto(methodType, machiningUri, authority, describe));
                }
            }

        }
    }

    private String machiningUri(String uri) {
        String[] strings = StringUtils.delimitedListToStringArray(uri, "/");

        return Arrays.stream(strings).map(new Function<String, String>() {
            @Override
            public String apply(String s) {
                if (s.contains("{")) {
                    return "*";
                }
                return s;
            }
        }).collect(Collectors.joining("/"));
    }


}

注意:由于布隆过滤器只能增加不能删除,所以需要每天或者几天进行一次数据更新

3. 编写刷新布隆过滤器数据定时器

通过spring定时器每天2点进行数据库刷新

@Slf4j
@Component
@EnableScheduling
public class BloomFilterRefresh {

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private ApplicationContext applicationContext;

    //设置每天凌晨2点进行更新
    @Scheduled(cron = "0 0 2 * * ?")
    public void bloomFilterValueUpdate(){
        //设置扫描包的路径
        List<String> list = scanClasses(this, "com.example");
        //设置布隆过滤器的原始大小
        Long bloomNum = 100_000L;
        for (String s : list) {
            try {
                //获取扫描下的类的类对象
                Class<?> aClass = Class.forName(s);
                //剔除掉没有注解的类的对象
                if (aClass.getAnnotation(BloomFilterScan.class)==null){
                    continue;
                }
                //更新布隆过滤器参数
                updateValue(bloomNum,aClass);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }


    private void updateValue(Long bloomNum, Class aClass) {
        //获取接口对象BaseMapper
        Class<?> anInterface= aClass.getInterfaces()[0];
        //从ioc容器中获取mysqlDao对象
        Object mysqlDao = applicationContext.getBean(aClass);

        Class entityClass = null;
        Method getId = null;
        try {
            //获取baseMapper的泛型类型
            ParameterizedType parameterizedType = (ParameterizedType) aClass.getGenericInterfaces()[0];
            entityClass = (Class) parameterizedType.getActualTypeArguments()[0];
            //找到对应getId方法
            getId = Arrays.stream(entityClass.getDeclaredMethods()).filter(method -> Objects.equals("getId", method.getName())).collect(Collectors.toList()).get(0);
        } catch (Exception e) {
            e.printStackTrace();
            //如果找不到方法,就跳出该循环
            return;

        }
        //存储获取的id集合
        List<Object> idList = new ArrayList<>();
        for (Method declaredMethod : anInterface.getDeclaredMethods()) {

            if ("selectList".equals(declaredMethod.getName())){
                try {
                    //获取数量的集合
                    List<Object> objectList = (List<Object>) declaredMethod.invoke(mysqlDao, new QueryWrapper<>());
                    //判断获取的集合数量是否大于设定的布隆过滤器数据量大小
                    if (objectList.size()> bloomNum){
                        //大于的话就扩大3倍
                        bloomNum = bloomNum *3L;
                    }
                    for (Object po : objectList) {
                        //运行getId方法获取对应id值
                        Object invoke = getId.invoke(po);
                        //存入到对应集合中
                        idList.add(invoke);
                    }
                } catch (Exception e) {
                    log.error(e.getMessage());
                }
            }
        }
        //设置布隆过滤器的key,目前为po类的名字
        RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(entityClass.getSimpleName());
        //给当前布隆过滤器的设置一个0秒的过期时间
        bloomFilter.expire(0, TimeUnit.SECONDS);
        //清除过去数据
        bloomFilter.clearExpire();
        //创建一个4%误差率,数量为自定义变量的空间
        bloomFilter.tryInit(bloomNum,0.005);
        //循环遍历id并存入到布隆过滤器中
        for (Object id : idList) {
            bloomFilter.add(id);
        }
        log.info("表 "+entityClass.getSimpleName()+" 数据的id已存入布隆过滤器中");
    }


    /**
     * 根据传入的根包名,扫描该包下所有类
     *
     * @param thiz            this
     * @param rootPackageName 包名
     */
    public static List<String> scanClasses(Object thiz, String rootPackageName) {
        return scanClasses(thiz.getClass(), rootPackageName);
    }



    /**
     * 根据传入的根包名,扫描该包下所有类
     *
     * @param thisClass       所在类
     * @param rootPackageName 包名
     */
    public static List<String> scanClasses(Class<?> thisClass, String rootPackageName) {
        return scanClasses(Objects.requireNonNull(thisClass.getClassLoader()), rootPackageName);
    }


    /**
     * 根据传入的根包名和对应classloader,扫描该包下所有类
     */
    public static List<String> scanClasses(ClassLoader classLoader, String packageName) {
        try {
            String packageResource = packageName.replace(".", "/");
            URL url = classLoader.getResource(packageResource);
            File root = new File(url.toURI());
            List<String> classList = new ArrayList<>();
            scanClassesInner(root, packageName, classList);
            return classList;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 遍历文件夹下所有.class文件,并转换成包名字符串的形式保存在结果List中。
     */
    private static void scanClassesInner(File root, String packageName, List<String> result) {
        for (File child : Objects.requireNonNull(root.listFiles())) {
            String name = child.getName();
            if (child.isDirectory()) {
                scanClassesInner(child, packageName + "." + name, result);
            } else if (name.endsWith(".class")) {
                String className = packageName + "." + name.replace(".class", "");
                result.add(className);
            }
        }
    }

}

你可能感兴趣的:(java,java,redis,数据库)