Redis实战——商户查询(二)

news2024/10/6 10:32:19

缓存穿透

缓存穿透 :客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这样的请求都会访问到数据库,这样的大量请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,对数据库造成巨大压力。

解决方案

  • 缓存空对象

    • 优点:实现简单,维护方便

    • 缺点:需要额外的内存消耗,可能造成短期数据不一致的情况

      缓存空对象
      缓存空对象

      当客户端请求不存在的数据时,请求先经过redis,redis中没有该数据,则进行数据库访问,但是发现数据库中也没有该数据,因为数据库能够承受的并发量有限,若大量的请求访问的都是这种不存在的数据,则都会访问到数据库,对数据库的压力很大,而缓存空对象的解决方案就是不管数据库中被访问的数据存在还是不存在,都将该数据写入缓存中,不存在的写入缓存时,其value置空即可,这样,下次用户过来访问这个不存在的数据时,redis缓存中也能找到该数据,不会访问到数据库。

  • 布隆过滤

    • 优点:内存占用较少

    • 缺点:实现复杂,存在误判的可能性

      布隆过滤器
      布隆过滤器

      布隆过滤器其实是采用的哈希思想来解决缓存穿透的问题,通过一个庞大的二进制数组,通过哈希思想去判断当前这个需要访问的数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会访问到redis缓存,若缓存中不存在,则去数据库中查询,并将结果写入缓存,并进行返回;如果布隆过滤器判断不存在,则直接返回。但是布隆过滤器会因为 哈希冲突 产生误判。

缓存空对象解决缓存穿透问题

解决缓存穿透 :如果客户端请求的数据缓存和数据库中都不存在,则将这个数据也写入缓存中,并将其value设置为空,若当再次发送请求查询该数据时,如果在缓存中命中,判断这个value是否是空,如果是空,则是之前写入的缓存穿透数据。

缓存空对象解决缓存穿透问题
缓存空对象解决缓存穿透问题
  • 修改根据id查询商铺信息

    @Override
    public Shop getShopById(Long id) {
        //组装redis中的key
        String cacheShopKey = CACHE_SHOP_KEY + id;
        //根据ID在redis中查询商铺信息
        String shopString = stringRedisTemplate.opsForValue().get(cacheShopKey);
        //redis中查询到商铺信息
        if (StrUtil.isNotBlank(shopString)){
            Shop shop = BeanUtil.toBean(shopString, Shop.class);
            return shop;
        }
        //根据商铺id查询商铺信息
        Shop shop = this.getById(id);
        //数据库中没查询到该商铺信息,则将空值写入缓存,并设置一个较短的TTL
        if (ObjectUtil.isNull(shop)){
            //数据库中查询到了该商铺信息,写入缓存,并设置有效时间为30分钟
            stringRedisTemplate.opsForValue().set(cacheShopKey, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //数据库中查询到了该商铺信息,写入缓存,并设置有效时间为30分钟
        stringRedisTemplate.opsForValue().set(cacheShopKey, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //返回给商铺信息
        return shop;
    }
  • 增加常量

    /**
     * redis中缓存一个空值的有效时间
     */

    public static final Long CACHE_NULL_TTL = 2L;

总结

  • 缓存穿透:用户请求的数据在缓存和数据库中都不存在,但用户还是不断的发送这样的请求,给数据库带来巨大压力

  • 解决方案

    • ​ ①缓存null值

    • ​ ② 布隆过滤器

    • ​ ③ 增强查询条件复杂度,避免被猜出

    • ​ ④ 完善数据的基础格式校验等

    • ​ ⑤ 加强用户权限校验

    • ​ ⑥ 做好热电参数的限流

缓存雪崩

  • 缓存雪崩 :是指在同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库。

    缓存雪崩-大量key过期-redis
    缓存雪崩-大量key过期-redis
    缓存雪崩-redis宕机
    缓存雪崩-redis宕机
  • 解决方案

    • 给不同key的TL添加随机值,避免大量数据TTL相同导致的同时过期
    • 搭建redis集群提高服务的可用性
    • 给缓存业务添加降级限流策略
    • 给业务添加多级缓存

缓存击穿

缓存击穿问题(热点key问题) :是指一个被高并发访问并且缓存重建业务比较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大压力。

image-20230705102137651
image-20230705102137651

线程1发送请求进行数据查询,查询缓存没有查询到数据,则去访问数据库,然后再将查询结果写入缓存,然而在线程1在访问数据库阶段,大量的线程也发送了该请求,因为缓存里没有数据,导致都去访问数据库,导致数据库压力过大。

  • 解决方案

    • 互斥锁

      image-20230705104416601
      image-20230705104416601

      根据锁的互斥性,来决定只有获得锁的访问请求才能够访问数据库,从而避免数据库访问压力过大,但互斥锁会影响查询性能, 一般采用的方案如下:线程1发送请求,查询缓存未命中,获取锁,然后查询数据库,而其他线程发送该请求,在线程1没有释放锁的情况下,其他线程只能等待(休眠),等待结束后,他们继续请求,先去查询缓存,再去获取锁,如果线程1释放了锁,那么其他发送该请求的线程在等待结束后都能在缓存中获取数据,如果线程1没释放锁,那么其他发送该请求的线程因为获取不到锁,会再次进入等待状态。

    • 逻辑过期

      image-20230705112108323
      image-20230705112108323

      逻辑过期指的是把过期时间设置在redis的value中,该时间不会作用于redis,而是后续通过逻辑去处理,假设线程1请求访问查询,先查询缓存,从查询的缓存value中判断当前数据是否逻辑过期,如果逻辑过期了,线程1获取互斥锁,并开启一个新的线程(线程2),线程2进行数据库访问,以及将数据重置逻辑过期时间写入缓存,该步骤完成后,进行锁的释放,假设其他线程在新数据没更新完成时访问,发现缓存数据已过期时,进行互斥锁的获取,如果获取不到互斥锁,则返回过期数据,或者线程2已将缓存进行更新,其他线程访问到的数据已是未过期数据。

优点缺点
互斥锁没有额外的内存消耗;
保证数据一致性;
实现简单
线程需要等待,性能受到一定影响;
有死锁风险
逻辑过期异步构建缓存,线程无需等待,性能良好不保证数据一致性,因为在异步构建缓存完成之前,返回的都是旧数据
有额外的内存消耗
实现复杂

互斥锁解决缓存击穿

  • 修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题

    互斥锁-缓存击穿
    互斥锁-缓存击穿
        /**
         * 获取锁
         * @param key
         * @return
         */

        public boolean tryLock(String key){
            return stringRedisTemplate.opsForValue().setIfAbsent(key,"1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        }

        /**
         * 释放锁
         * @param key
         * @throws Exception
         */

        public void unlock(String key) throws Exception {
            stringRedisTemplate.delete(key);
        }
        @Override
        public Shop getShopById(Long id) {
            //组装redis中的key
            String cacheShopKey = CACHE_SHOP_KEY + id;
            //根据ID在redis中查询商铺信息
            String shopString = stringRedisTemplate.opsForValue().get(cacheShopKey);
            //redis中查询到商铺信息
            if (StrUtil.isNotBlank(shopString)){
                Shop shop = BeanUtil.toBean(shopString, Shop.class);
                return shop;
            }
            //redis中没有获取到数据
            //获取互斥锁
            String lockShopKey = LOCK_SHOP_KEY + id;
            Shop shop = null;
            try {
                boolean b = tryLock(lockShopKey);
                //获取失败 休眠 重新请求
                if (!b){
                    Thread.sleep(30);
                    return getShopById(id);
                }
                //获取成功
                //获取锁成功应该进行再次检查redis缓存是否存在,即做DoubleCheck,如果存在则无需重建缓存
                //根据ID在redis中查询商铺信息
                shopString = stringRedisTemplate.opsForValue().get(cacheShopKey);
                //redis中查询到商铺信息
                if (StrUtil.isNotBlank(shopString)){
                    shop = BeanUtil.toBean(shopString, Shop.class);
                    return shop;
                }
                //根据商铺id查询商铺信息
                shop = this.getById(id);
                //数据库中没查询到该商铺信息,则将空值写入缓存,并设置一个较短的TTL
                if (ObjectUtil.isNull(shop)){
                    //数据库中查询到了该商铺信息,写入缓存,并设置有效时间为30分钟
                    stringRedisTemplate.opsForValue().set(cacheShopKey, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                    return null;
                }
                //数据库中查询到了该商铺信息,写入缓存,并设置有效时间为30分钟
                stringRedisTemplate.opsForValue().set(cacheShopKey, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
            }catch (Exception e){
                throw new RuntimeException(e);
            }finally {
                // 释放锁
                try {
                    unlock(lockShopKey);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            //返回给商铺信息
            return shop;
        }

    核心思路 :利用redis中的SETNX方法来表示获取锁,在stringRedisTemplate中,该方法被封装为setIfAbsent,当setIfAbsent方法返回true,则表明该key没有被存储在redis中,并且现在已经存储,此时成功存储key的线程就认为是获取锁的线程。

    • SETNX方法用于将一个键值对存储到redis中,但只有在指定的键不存在时才执行,如果键不存在,则不执行任何操作,
    • SETNX返回结果
      • 如果键名不存在并且存储成功,返回1
      • 如果键名已经存在,则返回0

逻辑过期解决缓存击穿

  • 修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

    逻辑有效时间-缓存击穿-提前缓存热点key
    逻辑有效时间-缓存击穿-提前缓存热点key
       /**
         * 将数据库查询到的数据设置逻辑过期时间 存储到redis中
         * @param id
         * @param expireTime
         */

        private void saveShop2Redis(Long id, Long expireTime){
            // 查询商铺信息
            Shop shop = this.getById(id);
            // 封装逻辑过期时间
            RedisData<Shop> redisData = new RedisData<>();
            redisData.setData(shop);
            redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
            //写入缓存
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
        }

        //Executors.newFixedThreadPool(10);也能创建线程池 但是不推荐
        //private static final ExecutorService a = Executors.newFixedThreadPool(10);
        // 创建线程池
        int corePoolSize = 10// 核心线程数
        int maxPoolSize = 20// 最大线程数
        long keepAliveTime = 60// 非核心线程的空闲时间
        TimeUnit unit = TimeUnit.SECONDS; // 时间单位
        LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(); // 任务队列

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue
        );
        @Override
        public Shop getShopById(Long id) {
            //组装redis中的key
            String cacheShopKey = CACHE_SHOP_KEY + id;
            //根据ID在redis中查询商铺信息
            String shopString = stringRedisTemplate.opsForValue().get(cacheShopKey);
            // 如果redis中不存在,则直接返回null
            if (StrUtil.isBlank(shopString)){
                return null;
            }
            // 不为空 判断是否逻辑过期
            RedisData<Shop> redisData = JSONUtil.toBean(shopString, RedisData.class);
            Shop shop = redisData.getData();
            LocalDateTime expireTime = redisData.getExpireTime();
    //        如果expireTime晚于当前时间,则返回true。
    //        如果expireTime早于或等于当前时间,则返回false。
            if (expireTime.isAfter(LocalDateTime.now())){
                //未过期 直接返回商铺信息
                return shop;
            }
            //过期
            //获取互斥锁
            String lockShopKey = LOCK_SHOP_KEY + id;
            try {
                boolean b = tryLock(lockShopKey);
                //互斥锁获取成功
                if (b) {
                //注意此处最好做彩瓷redis缓存是否过期的检查,即DoubleCheck,如果缓存没过期,则无需重建,主要防止他在获取锁的时候刚好有人重建完成导致的再次重建
                    //根据ID在redis中查询商铺信息
                    shopString = stringRedisTemplate.opsForValue().get(cacheShopKey);
                    // 如果redis中不存在,则直接返回null
                    if (StrUtil.isBlank(shopString)){
                        return null;
                    }
                    // 不为空 判断是否逻辑过期
                    redisData = JSONUtil.toBean(shopString, RedisData.class);
                    shop = redisData.getData();
                    expireTime = redisData.getExpireTime();
                    //如果expireTime晚于当前时间,则返回true。
                    //如果expireTime早于或等于当前时间,则返回false。
                    if (expireTime.isAfter(LocalDateTime.now())){
                        //未过期 直接返回商铺信息
                        return shop;
                    }
                    //开启新的线程
                    //重建缓存
                    executor.execute( ()->this.saveShop2Redis(id,20L));
                }
            }catch (Exception e){
                throw new RuntimeException();
            }finally {
                //释放锁
                try {
                    unlock(lockShopKey);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            //互斥锁获取失败,返回旧数据
            return shop;
        }

        /**
         * 获取锁
         * @param key
         * @return
         */

        public boolean tryLock(String key){
            return stringRedisTemplate.opsForValue().setIfAbsent(key,"1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        }

        /**
         * 释放锁
         * @param key
         * @throws Exception
         */

        public void unlock(String key) throws Exception {
            stringRedisTemplate.delete(key);
        }

    核心思路 :请求查询redis缓存,判断redis缓存中是否存在,如果不存在,则直接返回空数据,不查询数据库,如果存在,则判断数据中的逻辑过去时间是否已经过期,如果没有过期,直接返回redis中的数据,如果过期,则开启独立线程去重构数据。该方式需要 缓存预热

    • 缓存预热是指在系统启动或负载较低的时候,提前将一些常用的数据加载到缓存中,以减少后续请求的响应时间和系统压力。

    • 缓存预热的过程可以在系统启动时自动触发,也可以定期执行。下面是一种常见的缓存预热的实现方式:

      • 确定需要预热的数据:根据系统的特点和需求,确定哪些数据是频繁访问的、耗时较长的,可以考虑将这些数据预热到缓存中。

      • 在系统启动时或定期执行:在系统启动时或者定期触发预热任务,将需要预热的数据加载到缓存中。可以使用定时任务或者后台线程来实现。

      • 数据加载到缓存:根据对应的缓存方案,使用相应的接口将数据加载到缓存中。例如,使用Redis作为缓存,可以使用Redis的相关命令将数据写入缓存。

      • 预热完成标识:在预热完成后,可以设置一个标识来表示预热过程已经完成,以便系统其他部分知道缓存已经可用。

    通过缓存预热,可以提前将常用的数据加载到缓存中,在实际请求到来时直接从缓存中获取数据,减少了数据库或其他数据源的访问,提高了系统的性能和响应速度。

    需要注意的是,缓存预热可能会有一定的成本,包括预热数据的加载时间和系统资源消耗。因此,在选择需要预热的数据时,需要权衡数据的访问频率和成本,并根据实际情况评估是否值得进行缓存预热。

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/720818.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

js:使用diff.js实现文本内容差异比较

实现效果 目录 简介安装示例1、json比较diffJson2、按行比较diffLines3、比较数组diffArrays 总结参考资料 简介 A javascript text differencing implementation. 译文&#xff1a;javascript文本差异实现。 相关文档 github https://github.com/kpdecker/jsdiffnpmjs htt…

YoloV8改进---注意力机制:高斯上下文变换器GCT,性能优于ECA、SE等注意力模块 | CVPR2021

目录 1.GCT介绍 实验结果 2.GCT引入到yolov8 2.1 修改modules.py中&#xff1a; 2.2 加入tasks.py中&#xff1a; 2.3 yolov8_GCT.yaml 3.YOLOv8魔术师专栏介绍 1.GCT介绍 论文&#xff1a;https://openaccess.thecvf.com/content/CVPR2021/papers/Ruan_Gaussian_Context_…

MySQL数据库架构

MySQL数据库架构 MySQL架构自顶向下大致可以分为连接层 , SQL层 , 存储引擎层 , 物理文件层。架构如下 连接层 -- 查看最大连接数 show variables like %max_connections%;客户端连接器&#xff0c;MySQL向外提供交互接口连接各种不同的客户端。 客户端/应用程序&#xff1a;客…

7.3.6 【Linux】磁盘/文件系统参数修订

mknod 用到的磁盘 /dev/vda 的相关设备代码如下&#xff1a; 上表当中 252 为主要设备代码 &#xff08;Major&#xff09; 而 0~5 则为次要设备代码 &#xff08;Minor&#xff09;。 我们的Linux 核心认识的设备数据就是通过这两个数值来决定的&#xff01;举例来说&#xf…

如何批量将PDF转换为图片?

在生活工作中&#xff0c;我们会处理很多电子合同。这些电子合同一般是PDF格式&#xff0c;不但存储空间大&#xff0c;且预览起来不太便捷&#xff0c;需要我们转换为图片格式更方便预览。如果人工一一处理比较繁琐复杂&#xff0c;有没有什么方案可以快速将pdf转换为图片呢&a…

如果想用unity做一个项目作为面试作品,至少该达到什么样的标准?

本文仅针对题目“如果想用unity做一个项目作为面试作品&#xff0c;至少该达到什么样的标准&#xff1f;”回答内容。 明确职业目标 首先要明确自身的职业目标&#xff0c;不同的行业、公司、游戏类型、岗位对作品的要求是不同的。 去什么样的行业&#xff1f;unity可做的有很…

python的IOError使用

try:npzfile np.load(calibrate.npz)mtx npzfile[mtx]dist npzfile[dist]except IOError:calibrate()python语言IOError错误一般多发生在对文件操作报错时&#xff0c;表示要打开的文件不存在

PostpreSQL内核学习-Chapter4 Foreign Data Wrapper(FDW)

FDW连接 preparations 两个在不同主机&#xff08;满足TCP/IP连接是分属于不同IP地址下&#xff09;&#xff08;如果是安装PG源码&#xff09;进入源码目录下面的/contrib/postgres_fdw/&#xff0c;然后用make & make install编译和安装 [rootlocalhost /]# cd 源码存…

为什么truncate函数(四舍五入)是x+0.4999997而不是+0.5?

目录 float的不精确表示 0.5的舍入方法 该方法的漏洞 0.4999997f舍入的结果错误 以0.4999997f改进舍入方法 可以用0.49999996、0.49999998或者0.49999999替换0.49999997吗&#xff1f; 在做舍入函数研究时&#xff0c;发现函数中实现四舍五入的trunc函数大概采用的逻辑是floor(…

LVS负载均衡-----DR模式

目录 1.DR模式原理 2.DR 模式的特点&#xff1a; 3.LVS-DR中的ARP问题 问题原因&#xff1a; 解决方法&#xff1a; 问题二&#xff1a;返回报文时源地址使用VIP&#xff0c;导致网关设备的ARP缓存表紊乱 解决方法&#xff1a; 4.DR模式 LVS负载均衡群集部署 1.配置负…

python进程线程问题

参考链接&#xff1a; 代码可去原博文复制&#xff1a; python缩短大量数据处理时间-进程池pool()和map() 一 定义与解释 1、对于python代码&#xff0c;多线程其实是个假的&#xff0c;因为每次计算的时候&#xff0c;实质上只有一个线程计算。使用多线程时&#xff0c;是几…

【天池题解】题解:CAAI-BDSC2023社交图谱链接预测(任务一:社交图谱小样本场景链接预测)

【天池题解】题解&#xff1a;CAAI-BDSC2023社交图谱链接预测&#xff08;任务一&#xff1a;社交图谱小样本场景链接预测&#xff09; 文章目录 【天池题解】题解&#xff1a;CAAI-BDSC2023社交图谱链接预测&#xff08;任务一&#xff1a;社交图谱小样本场景链接预测&#xf…

中介者模式的学习与使用

1、中介者模式的学习 中介者模式是一种行为型设计模式&#xff0c;它通过引入一个中介者对象来解耦一组对象之间的交互。中介者模式促进了对象之间的松耦合&#xff0c;使得它们可以独立地进行通信&#xff0c;而不需要直接相互引用。   在中介者模式中&#xff0c;有以下几个…

获取文件大小并转换单位

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

【工具使用】Notepad++如何使用正则表达式同时搜索多个字符串

一&#xff0c;简介 在调试代码定位问题的时候&#xff0c;由于log打印比较多&#xff0c;故需要使用NotePad文本编辑器进行搜索&#xff0c;并且需要同时搜索多个字符串&#xff0c;本文介绍如何同时搜索多个字符串&#xff0c;供参考。 二&#xff0c;操作步骤 2.1 搜索设…

90、基于STM32单片机数字频率计频率检测配NE555脉冲发生器设计(程序+原理图+PCB源文件+参考论文+硬件设计资料+元器件清单等)

单片机主芯片选择方案 方案一&#xff1a;AT89C51是美国ATMEL公司生产的低电压&#xff0c;高性能CMOS型8位单片机&#xff0c;器件采用ATMEL公司的高密度、非易失性存储技术生产&#xff0c;兼容标准MCS-51指令系统&#xff0c;片内置通用8位中央处理器(CPU)和Flash存储单元&a…

如何基于 ESP-IDF SDK 为应用工程添加自定义的 menuconfig 编译条件设置?

在工程的 main 目录下添加 Kconfig.projbuild 文件对需要添加自定义设置的参数进行定义在应用代码中使用自定义设置的参数编写 Kconfig.projbuild 文件 在工程的 main 目录下添加 Kconfig.projbuild 文件 接下来会基于 esp-idf/examples/peripherals/uart/uart_async_rxtxtask…

Python黑魔法揭秘:超强公共操作符和推导式的编程技巧

文章目录 前言公共操作1.运算符1&#xff09; 合并操作2&#xff09;* 复制操作in / not in 判断数据在 / 不在容器中 公共方法len()del / del()max()min()range()enumerate() 容器类型转换1&#xff09;list() 将某个序列转化为列表tuple() 将某个序列转换为元组set() 将某个序…

OpenCloudOS社区开源,助力软件开发

早前红帽宣布限制源代码访问性的政策&#xff0c;并解释说RHEL相关源码仅通过CentOS Stream公开&#xff0c;付费客户和合作伙伴可通过Red Hat Customer Portal访问到源代码&#xff0c;由此也导致非客户获取源码越来越麻烦&#xff0c; 据了解&#xff0c;CentOS是红帽发行的…

效率为王!项目管理软件如何帮你提高效率?

对于任何商业组织及其客户来说&#xff0c;低效率的员工和浪费的时间都是一个问题。团队成员懈怠会对团队的整体效率产生负面影响。除了巨大的财务挫折之外&#xff0c;这些问题的混合还导致多次延误、工作质量差和客户不满意。 但有了正确的项目管理软件&#xff0c;企业就可…