首页 > 其他分享 >缓存优化(缓存穿透)

缓存优化(缓存穿透)

时间:2024-07-28 22:29:40浏览次数:12  
标签:缓存 布隆 private 穿透 哈希 过滤器 优化 public

缓存优化(缓存穿透)

缓存穿透

  • 缓存穿透是指查询一个一定不存在的数据时,数据库查询不到数据,也不会写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,可能导致数据库崩溃。
  • 这种情况大概率是遭到了攻击。
  • 常见的解决方案有:缓存空数据,使用布隆过滤器等。

当前项目中存在的问题

  • 当前项目中,用户端会大量访问的数据,即菜品数据和套餐数据,会被按照其分类id缓存在redis中。当用户查询某个分类下的菜品时,首先会查询redis中是否有这个分类id的数据,如果有就直接返回,否则就去查询数据库并将查询到的数据返回,同时会将查询到的数据存入redis缓存。

  • 假如查询的数据在数据库中不存在,那么就会将空数据缓存在redis中。

  • 缓存空数据的优点是实现简单,但是它也有缺点:会消耗额外的内存。尤其是在当前项目中,缓存在redis中的数据都没有设置过期时间,因此缓存的空数据只会越来越多,直到把redis的内存中间占满。

解决方案

使用redisson提供的布隆过滤器作为拦截器,拒绝掉不存在的数据的绝大多数查询请求。

布隆过滤器

布隆过滤器主要用于检索一个元素是否在一个集合中。如果布隆过滤器认为该元素不存在,那么它就一定不存在。

底层数据结构

  • 布隆过滤器的底层数据结构为bitmap+哈希映射。
  • bitmap其实就是一个数组,它只存储二进制数0或1,也就是按位(bit)存储。哈希映射就是使用哈希函数将原来的数据映射为哈希值,一个数据只对应一个哈希值,这样就能够判断之后输入的值是不是原来的数据了。但是哈希映射可能会出现哈希冲突,即多个数据映射为同一个哈希值。

具体的算法步骤为:

  1. 初始化一个较大的bitmap,每个索引的初始值为0,并指定数个哈希函数(比如3个)。
  2. 存储数据时,使用这些哈希函数处理输入的数据得到多个哈希值,再使用这些哈希值模上bitmap的大小,将余数位置的值置为1。
  3. 查询数据时,使用相同的哈希函数处理输入的数据得到多个哈希值,模上bitmap的大小后判断对应位置是否都为1,如果有一个不为1,布隆过滤器就认为这个数不存在。
  • 由于哈希映射本来就存在哈希冲突,并且查询时,计算得到的索引处的值虽然都是1,但却是不同数据计算得到的。所以布隆过滤器存在误判。数组越大误判率越小,数组越小误判率越大,我们可以通过调整数组大小来控制误判率。

优点

内存占用较少,缓存中没有多余的空数据键。

缺点

实现复杂,存在误判,难以删除。

代码开发

配置redisson

  1. 在pom.xml中引入redisson的依赖:
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.33.0</version>
</dependency>
  1. 在sky-commom包下的com.sky.properties包下创建RedisProperties类,用于配置redisson:
@Component
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedisProperties {

    /**
     * redis相关配置
     */
    private String host;
    private String port;
    private String password;
    private int database;

}
  1. 配置redisson,在com.sky.config包下创建RedissonConfiguration类并创建redissonClient的Bean对象:
@Configuration
@Slf4j
public class RedissionConfiguration {

    @Autowired
    private RedisProperties redisProperties;

    @Bean
    public RedissonClient redissonClient() {
        log.info("开始创建redisson客户端对象...");

        //拼接redis地址
        StringBuffer address = new StringBuffer("redis://");
        address.append(redisProperties.getHost()).append(":").append(redisProperties.getPort());

        //创建并配置redisson客户端对象
        Config config = new Config();
        config.setCodec(StringCodec.INSTANCE)
                .useSingleServer()
                .setAddress(address.toString())
                .setPassword(redisProperties.getPassword())
                .setDatabase(redisProperties.getDatabase());
        return Redisson.create(config);
    }
}

配置布隆过滤器

  1. 在配置文件application.yml中引入布隆过滤器相关配置:
spring
  bloom-filter:
    expected-insertions: 100
    false-probability: 0.01
  1. 配置布隆过滤器,在com.sky.config包下创建BloomFilterConfiguration类并创建bloomFilter的Bean对象:
@Configuration
@Slf4j
public class BloomFilterConfiguration {

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private CategoryMapper categoryMapper;
    @Value("${spring.bloom-filter.expected-insertions}")
    private long expectedInsertions;
    @Value("${spring.bloom-filter.false-probability}")
    private double falseProbability;

    /**
     * 创建并预热布隆过滤器
     */
    @Bean("bloomFilter")
    public RBloomFilter<Integer> init() {
        log.info("开始创建布隆过滤器...");
        RBloomFilter<Integer> bloomFilter = redissonClient.getBloomFilter("BloomFilter", StringCodec.INSTANCE);
        bloomFilter.tryInit(expectedInsertions, falseProbability);

        log.info("开始预热布隆过滤器...");

        //查询所有的分类id
        List<Integer> CategoryIds = categoryMapper.getCategoryIdsByStatus(StatusConstant.ENABLE);

        //预热布隆过滤器
        bloomFilter.add(CategoryIds);

        return bloomFilter;
    }
}
  1. 在CategoryMapper接口中创建getCategoryIdsByStatus方法:
@Select("select id from category where status = #{status}")
List<Integer> getCategoryIdsByStatus(Integer status);

配置布隆过滤器的拦截器

  1. 在com.sky.interceptor包下创建BloomFilterInterceptor类并编写布隆过滤器拦截器的校验逻辑:
@Component
@Slf4j
public class BloomFilterInterceptor implements HandlerInterceptor {

    @Autowired
    private RBloomFilter bloomFilter;

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从查询参数中获取分类id
        String categoryId = request.getQueryString().split("=")[1];

        //2、校验分类id
        try {
            log.info("布隆过滤器校验:{}", categoryId);
            if (bloomFilter.contains(Integer.valueOf(categoryId))) {
                //3、通过,放行
                log.info("布隆过滤器校验通过");
                return true;
            } else {
                //4、不通过,抛出分类不存在异常
                log.info("布隆过滤器校验不通过");
                throw new CategoryIdNotFoundException(MessageConstant.CATEGORY_ID_NOT_FOUND);
            }
        } catch (Exception ex) {
            //4、并响应404状态码
            response.setStatus(404);
            return false;
        }
    }
}
  1. 在MessageConstant类中增加一条信息提示常量,用于拦截器抛出:
public class MessageConstant {
	...
	public static final String CATEGORY_ID_NOT_FOUND = "分类不存在";
}
  1. 在sky-common模块下的com.sky.exception包下创建新的异常类CategoryIdNotFoundException类,用于拦截器抛出:
public class CategoryIdNotFoundException extends BaseException {

    public CategoryIdNotFoundException() {
    }

    public CategoryIdNotFoundException(String msg) {
        super(msg);
    }
}

注册布隆过滤器的拦截器

  1. 在配置类WebMvcConfiguration中注册BloomFilterInterception:
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    ...
    @Autowired
    private BloomFilterInterceptor bloomFilterInterceptor;
    
    protected void addInterceptors(InterceptorRegistry registry) {
        ...
        registry.addInterceptor(bloomFilterInterceptor)
                .addPathPatterns("/user/setmeal/list")
                .addPathPatterns("/user/dish/list");
    }
    ...
}

使用AOP来更新布隆过滤器

当数据库里的分类数据发生改变时,布隆过滤器也要相应的更新,由于布隆过滤器难以删除元素,所以更新步骤为:

  1. 将布隆过滤器里所有键的过期时间都设置为现在。
  2. 清理布隆过滤器里所有过期的键。
  3. 重新预热布隆过滤器。

代码如下:

  1. 在com.sky.annotation包下自定义注解UpdateBloomFilter,用于标识某个方法执行完成后需要更新布隆过滤器:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UpdateBloomFilter {}
  1. 在com.sky.service.aspect包下创建切面类UpdateBloomFilterAspect,用于更新布隆过滤器:
@Aspect
@Component
@Slf4j
public class UpdateBloomFilterAspect {

    @Autowired
    private ApplicationContext applicationContext;
    @Value("${spring.bloom-filter.expected-insertions}")
    private long expectedInsertions;
    @Value("${spring.bloom-filter.false-probability}")
    private double falseProbability;
    @Autowired
    private CategoryMapper categoryMapper;

    /**
     * 切入点
     */
    @Pointcut("@annotation(com.sky.annotation.UpdateBloomFilter)")
    public void updateBloomFilterPointcut() {}

    /**
     * 后置通知,在通知中进行布隆过滤器的更新
     */
    @After("updateBloomFilterPointcut()")
    public void updateBloomFilter(JoinPoint joinPoint) {
        log.info("开始更新布隆过滤器");

        //获得布隆过滤器的Bean对象
        RBloomFilter<Integer> bloomFilter = (RBloomFilter<Integer>) applicationContext.getBean("bloomFilter");
        //清理布隆过滤器
        bloomFilter.expire(Instant.now());
        bloomFilter.clearExpire();
        //初始化布隆过滤器
        bloomFilter.tryInit(expectedInsertions, falseProbability);

        log.info("开始预热布隆过滤器...");

        //查询所有的分类id
        List<Integer> CategoryIds = categoryMapper.getCategoryIdsByStatus(StatusConstant.ENABLE);

        //预热布隆过滤器
        bloomFilter.add(CategoryIds);
    }
}
  1. 在CategoryController类中的新增分类、删除分类和启用禁用分类方法上加上注解@UpdateBloomFilter:
public class CategoryController {
    ...
    @UpdateBloomFilter
    public Result<String> save(@RequestBody CategoryDTO categoryDTO) {...}
    
    @UpdateBloomFilter
    public Result<String> deleteById(Long id) {...}
    
    @UpdateBloomFilter
    public Result<String> startOrStop(@PathVariable("status") Integer status, Long id) {...}
    ...
}

功能测试

布隆过滤器的拦截器功能验证

通过接口文档测试,并观察日志来进行验证:

  • 当前端查询数据库里存在的数据时:

  • 当前端查询数据库里不存在的数据时:

布隆过滤器的更新功能验证

通过接口文档测试或前后端联调测试,并观察日志和redis缓存进行验证:

  • 日志:

  • redis缓存更新之前:
  • redis缓存更新之后:

标签:缓存,布隆,private,穿透,哈希,过滤器,优化,public
From: https://www.cnblogs.com/zgg1h/p/18328969

相关文章

  • vue2 - 详细实现“视频切片/分段加载“播放大视频,解决视频过大加载播放缓慢问题,vue处
    效果图在vue2、nuxt2项目开发中,详解vue视频分片加载,所谓“边播放边加载”,利用axios分段请求后端服务器每次只拿一小段视频慢慢缓存播放,让非常大的视频(例如电影,很长的视频播放太慢)流畅播放,vue2实现将video视频进行切片网络请求加载提升视频加载速度,详细解决视频分段下载......
  • IDEA的常用配置和优化
    文章目录IDEA的常用配置和优化IntelliJIDEA版本:2019.2.3永久破解需要以下两个破解文件选择Evaluateforfree(试用)启动IDEA,在启动界面上点击Configure-->EditCustomVMOptions...重置Idea试用开始时间重置插件安装重置插件设置idea性能优化--自定义虚拟机内存Ma......
  • 深度模型中的优化 - 基本算法篇
    序言在深度学习中,模型优化是提升模型性能与训练效率的关键环节。深度模型通过优化算法不断调整其内部参数,以最小化损失函数,从而实现对复杂数据的有效拟合与预测。本篇章将简要概述深度模型中的几种基本优化算法,包括梯度下降法及其变种,这些算法在推动深度学习领域的发展中起......
  • Memcached跨平台性能解码:操作系统对缓存速度的影响
    Memcached跨平台性能解码:操作系统对缓存速度的影响在分布式缓存系统的设计和部署中,Memcached因其轻量级和高性能而成为首选方案之一。然而,Memcached在不同操作系统上的性能表现可能会有显著差异。本文将深入探讨这些差异的原因,并提供实际的测试方法和代码示例,帮助系统架构......
  • 科普文:详解 JuiceFS 读性能:预读、预取、缓存、FUSE 和对象存储
    在高性能计算场景中,往往采用全闪存架构和内核态并行文件系统,以满足性能要求。随着数据规模的增加和分布式系统集群规模的增加,全闪存的高成本和内核客户端的运维复杂性成为主要挑战。JuiceFS,是一款全用户态的云原生分布式文件系统,通过分布式缓存大幅提升I/O吞吐量,并使用成本......
  • Linux——手动清理内存缓存
    前言:使用free-m命令可以查看内存缓存。一、方法1.1先进管理员账户,然后进root账户1.2运行下面的命令:syncecho1>/proc/sys/vm/drop_caches#清空目录项缓存echo0>/proc/sys/vm/drop_caches#还原默认配置,这一步如果出错,则不用管sync二、小贴士......
  • 以BGP方式直连中国联通骨干网络,为客户提供全方位的互联网穿透服务
    联通IPTransit产品:以BGP直连,引领互联网穿透新纪元在数字化转型的浪潮中,互联网已成为连接世界、推动各行业创新发展的核心基础设施。面对日益增长的网络流量需求与复杂多变的网络环境,如何高效、安全地将内容推向全球用户,成为内容提供商面临的重要挑战。中国联通,作为国内领先的电......
  • JVM垃圾回收器和优化
    JVM(JavaVirtualMachine)垃圾回收器(GarbageCollector,GC)是Java内存管理中的重要组成部分,负责自动回收不再被使用的内存空间,以防止内存泄露和内存溢出。同时,JVM的优化也是提高Java应用程序性能的重要手段。以下将详细介绍JVM垃圾回收器的种类以及JVM的优化方法。一、JVM垃圾......
  • Nginx 如何实现请求的缓存过期策略?
    ......
  • 18、flask-进阶-插件-缓存flask-caching - 钩子函数(中间件)
    1.认识flask-caching插件使用插件1.安装$flaskinstallflask-caching2.初始化在exts.py中导入并初始化fromflask_cachingimportCache#初始化插件cache=Cache(config={'CACHE_TYPE':'simple'#缓存类型})#和app对象绑定definit_exts(app):......