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

缓存穿透

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

解决方案

  • 缓存空对象

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

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

      缓存空对象
      缓存空对象

      当客户端请求不存在的数据时,请求先经过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 多平台发布

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于smardaten无代码开发舆情分析系统

一、前言 在日常生活中&#xff0c;有各种各样的资讯、社交平台。这些平台充斥着大量信息&#xff0c;这些信息中隐含了许多有用数据&#xff0c;但是这些数据无法之间获取&#xff0c;且难以展示&#xff0c;于是就有了舆情分析系统。 舆情分析系统是一个综合的系统&#xf…

基于minsit数据集的图像分类任务|CNN简单应用项目

Github地址 Image-classification-task-based-on-minsit-datasethttps://github.com/Yufccode/CollegeWorks/tree/main/ImageProcessing/Image-classification-task-based-on-minsit-dataset README 摘要 本次实验报告用两种方式完成了基于minst数据集完成了图像的分类任务…

被吐槽 GitHub仓 库太大,直接 600M 瘦身到 6M,这下舒服了

前言 忙里偷闲学习了点技术写了点demo代码&#xff0c;打算提交到我那 2000Star 的Github仓库上&#xff0c;居然发现有5个Issues&#xff0c;最近的一条日期已经是2022/8/1了&#xff0c;以前我还真没留意过这些&#xff0c;我这人懒得很&#xff0c;本地代码提交成功基本就不…

Python dict keys方法:获取字典中键的序列【将keys转为list】

描述 dict.keys()方法是Python的字典方法&#xff0c;它将字典中的所有键组成一个可迭代序列并返回。 使用示例 >>> list({Chinasoft:China, Microsoft:USA}.keys()) [Chinasoft, Microsoft] >>> test_dict {Chinasoft:China, Microsoft:USA, Sony:Japan,…

【计算机视觉 | 目标检测】arxiv 计算机视觉关于目标检测的学术速递(7 月 4 日论文合集)

文章目录 一、检测相关(15篇)1.1 Artifacts Mapping: Multi-Modal Semantic Mapping for Object Detection and 3D Localization1.2 Shi-NeSS: Detecting Good and Stable Keypoints with a Neural Stability Score1.3 HODINet: High-Order Discrepant Interaction Network for…

机器学习一:线性回归

1 知识预警 1.1 线性代数 ( A T ) T A (A^\mathrm{T})^\mathrm{T}A (AT)TA$ ( A B ) T A T B T (AB)^\mathrm{T}A^\mathrm{T}B^\mathrm{T} (AB)TATBT ( λ A ) T λ A T (\lambda A)^\mathrm{T}\lambda A^\mathrm{T} (λA)TλAT ( A B ) T B T A T (AB)^\mathrm{T}B^…

【算法与数据结构】28、LeetCode实现strStr函数

文章目录 一、题目二、暴力穷解法三、KMP算法四、Sunday算法五、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、暴力穷解法 思路分析&#xff1a;首先判断字符串是否合法&#xff0c;然后利用for循环&#xff…

2023年全国节能宣传“节能低碳,你我同行”主题有奖竞答

2023年的7月10日至16日是第33个全国节能宣传周&#xff0c;主题是“节能降碳&#xff0c;你我同行”。 为践行低碳生活&#xff0c;切实做到节能降碳&#xff0c;各大企事业单位纷纷举办“节能低碳&#xff0c;你我同行”主题2023年全国节能宣传有奖竞答。 有奖知识竞答活动方…

Prometheus实现自定义指标监控

1、Prometheus实现自定义指标监控 前面我们已经通过 PrometheusGrafana 实现了监控&#xff0c;可以在 Grafana 上看到对应的 SpringBoot 应用信息了&#xff0c; 通过这些信息我们可以对 SpringBoot 应用有更全面的监控。 但是如果我们需要对一些业务指标做监控&#xff0c;…

【AI实战】从零开始搭建中文 LLaMA-33B 语言模型 Chinese-LLaMA-Alpaca-33B

【AI实战】从零开始搭建中文 LLaMA-33B 语言模型 Chinese-LLaMA-Alpaca-33B 简介环境配置环境搭建依赖安装 代码及模型权重拉取拉取 Chinese-LLaMA-Alpaca拉取 llama-30b-hf 模型权重及代码拉取 chinese-llama-lora-33b 模型权重及代码 合并模型权重先转换 pth 类型的模型权重&…

只出现一次的数字

题目链接 只出现一次的数字 题目描述 注意点 1 < nums.length < 30000-30000 < nums[i] < 30000除了某个元素只出现一次以外&#xff0c;其余每个元素均出现两次 解答思路 最初想到使用一种数据结构将元素存储起来&#xff0c;但是空间复杂度为O(n)&#xff0…

【花雕】全国青少年机器人技术一级考试备考实操搭建手册6

随着科技的不断进步&#xff0c;机器人技术已经成为了一个重要的领域。在这个领域中&#xff0c;机械结构是机器人设计中至关重要的一部分&#xff0c;它决定了机器人的形态、运动方式和工作效率。对于青少年机器人爱好者来说&#xff0c;了解机械结构的基础知识&#xff0c;掌…

大语言模型的百家齐放

基础语言模型 概念 基础语言模型是指只在大规模文本语料中进行了预训练的模型&#xff0c;未经过指令和下游任务微调、以及人类反馈等任何对齐优化。 如何理解 只包含纯粹的语言表示能力,没有指导性或特定目标。 只在大量无标注文本上进行无监督预训练,用于学习语言表示。 …

git 新建分支,切换分支,上传到远程分支

git 在使用的过程中&#xff0c;有的时候我们需要更换一个分支才存贮数据&#xff0c;作为版本的一个迭代或者是阶段性成果的一个里程碑。 如何来做操作呢&#xff1f; 在git中&#xff0c;可利用checkout命令转换分支&#xff0c;该命令的作用就是切换分支或恢复工作树文件&a…

【微信小程序开发】第 9 课 - 小程序的协同工作和发布

欢迎来到博主 Apeiron 的博客&#xff0c;祝您旅程愉快 &#xff01; 时止则止&#xff0c;时行则行。动静不失其时&#xff0c;其道光明。 目录 1、协同工作 1.1、了解权限管理需求 1.2、了解项目成员的组织结构 1.3、小程序的开发流程 2、小程序成员管理 2.1、成员管…

Nftables栈溢出漏洞(CVE-2022-1015)复现

背景介绍 Nftables Nftables 是一个基于内核的包过滤框架&#xff0c;用于 Linux 操作系统中的网络安全和防火墙功能。nftables 的设计目标是提供一种更简单、更灵活和更高效的方式来管理网络数据包的流量。 钩子点&#xff08;Hook Point&#xff09; 钩子点的作用是拦截数…

DMDSC共享存储集群启动、关闭及介绍

DMDSC介绍 DM 共享存储数据库集群&#xff08;DMDSC&#xff09;。DM共享存储数据库集群&#xff0c;允许多个数据库实例同时访问、操作同一数据库&#xff0c;具有高可用、高性能、负载均衡等特性。DMDSC 支持故障自动切换和故障自动重加入&#xff0c;某一个数据库实例故障后…

使用GeoPandas进行地理空间数据可视化

大家好&#xff0c;在当今数据驱动的世界中&#xff0c;将信息可视化到地图上可以提供有价值的见解&#xff0c;帮助有效地传达复杂的模式。GeoPandas是一个建立在pandas和shapely之上的Python库&#xff0c;使用户能够通过将地理空间数据与各种变量合并来创建令人惊叹的地图。…

深度学习(23)——YOLO系列(2)

深度学习&#xff08;23&#xff09;——YOLO系列&#xff08;2&#xff09; 文章目录 深度学习&#xff08;23&#xff09;——YOLO系列&#xff08;2&#xff09;1. model2. dataset3. utils4. test/detect5. detect全过程 今天先写YOLO v3的代码&#xff0c;后面再出v5&…

C语言:猜凶手

题目&#xff1a; 日本某地发生了一件谋杀案&#xff0c;警察通过排查确定杀人凶手必为4个嫌疑犯的一个。 以下为4个嫌疑犯的供词: A说&#xff1a;不是我。 B说&#xff1a;是C。 C说&#xff1a;是D。 D说&#xff1a;C在胡说 已知3个人说了真话&#xff0c;1个人说的是假话。…