案例解决Redis高并发场景带来的缓存穿透、击穿、雪崩问题(超级详细!!)

news2024/12/26 21:53:06

假设你的网站流量量达到亿级,传统的去查询DB势必会给DB带来巨大的压力,甚至可能有宕机的风险,接下来我就分几个阶段,来讲诉各个场景可能会给DB带来巨大压力的可能,以及优化的方案。

  • 缓存击穿:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
  • 缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
  • 缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

场景导入

* 最初的数据Redis的数据查询方法
* 假设是查询product 产品的信息
* 场景:当商品的数据有几亿的时候,直接请求到数据库,会造成数据库的压力过大,而产生宕机风险


    private final static String PRODUCT_NAME_KEY = "product:name:key:";

    public PatrolTask one(Long productId) {
        PatrolTask patrolTask;
        String productCacheKey = PRODUCT_NAME_KEY + productId;
        //去缓存中找到对应的产品数据,并取出
        String productStr = redisUtil.get(productCacheKey);

        if (C.isNotEmpty(productStr)) {
            patrolTask = JSONObject.parseObject(productStr, PatrolTask.class);
            return patrolTask;
        }

        PatrolTask dbData = iPatrolTaskService.getById(productId);
        if (C.isNotEmpty(dbData)) {
            //如果去数据库查找到的商品是不空的情况下,把数据再次放入到缓存中
            redisUtil.set(productCacheKey, JSON.toJSON(dbData));
        }
        return dbData;
    }

以上代码可能会导致的问题:  

* 上面的会造成的问题,如果把几亿的数据量都放入的缓存中

* 但是常用的就那么几个商品,就会占用大量的内存资源,消耗内存

第二次改造

给放入的缓存加入一定的过期时间,这样不常用的数据到期就会自动删除

    public PatrolTask two(Long productId) {
        PatrolTask patrolTask;
        String productCacheKey = PRODUCT_NAME_KEY + productId;
        String productStr = redisUtil.get(productCacheKey);

        if (C.isNotEmpty(productStr)) {
            patrolTask = JSONObject.parseObject(productStr, PatrolTask.class);
            return patrolTask;
        }

        PatrolTask dbData = iPatrolTaskService.getById(productId);
        if (C.isNotEmpty(dbData)) {
            //放入缓存中时,加入过期时间,防止数量过大
            redisUtil.set(productCacheKey, JSON.toJSON(dbData), PRODUCT_TIME);
        }
        return dbData;
    }

以上代码可能会导致的问题:  

* 以上代码存在问题:如果说我批量导入10000个产品,在缓存中的商品会同一时间都过期

* 而这个时间又有大量的请求打入来查询商品 就会造成了缓存雪崩现象 

缓存雪崩

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力

第三次改造

 给过期时间设置随机值,不让他们同一时间失效,或者是热点缓存永不过期

    public PatrolTask third(Long productId) {
        PatrolTask patrolTask;
        String productCacheKey = PRODUCT_NAME_KEY + productId;
        String productStr = redisUtil.get(productCacheKey);

        if (C.isNotEmpty(productStr)) {
            patrolTask = JSONObject.parseObject(productStr, PatrolTask.class);
            return patrolTask;
        }

        PatrolTask dbData = iPatrolTaskService.getById(productId);
        if (C.isNotEmpty(dbData)) {
            //设置缓存时间的随机
            redisUtil.set(productCacheKey, JSON.toJSON(dbData), getProductCacheTime());
        }
        return dbData;
    }


    //设置缓存时间的随机值
    private Long getProductCacheTime() {
        return PRODUCT_TIME + new Random().nextInt(30) * 60;

    }

 以上代码可能会导致的问题:  

* 以上代码存在问题:如果运营人员现在把热点的商品进行了删除,但是这个时间又有大量的人来访问这个商品。
* 而在缓存中都查不到,就回去查数据库,就会给数据库造成巨大压力,这种现象就叫做缓存穿透

缓存穿透

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库

第四次改造

解决的办法,就是如果说数据库查不到,我们就返回一个空对象,存入缓存中,防止大量数据库直接打入数据库

    public PatrolTask four(Long productId) {
        PatrolTask patrolTask;
        String productCacheKey = PRODUCT_NAME_KEY + productId;
        String productStr = redisUtil.get(productCacheKey);

        if (C.isNotEmpty(productStr)) {
            //如果查到的值为空,就直接进行返回一个空的对象
            if (PRODUCT_EMPTY.equals(productStr)) {
                return new PatrolTask();
            }
            patrolTask = JSONObject.parseObject(productStr, PatrolTask.class);
            return patrolTask;
        }

        PatrolTask dbData = iPatrolTaskService.getById(productId);
        if (C.isNotEmpty(dbData)) {

            redisUtil.set(productCacheKey, JSON.toJSON(dbData), getProductCacheTime());
        } else {
            //缓存一个空的值
            redisUtil.set(productCacheKey, PRODUCT_EMPTY, getProductCacheTime());

        }

        return dbData;
    }

 以上代码可能会存在的问题:  

* 以上代码存在问题:如果说之前在缓存中没有这个数据,那么之前数据就有可能没有在缓存中
* 但是突然这个数据并发了大量的并发,就有可能大量的同一时间去创建这个缓存数据,就会给系统造成压力暴增问题,就会出现缓存击穿现象

缓存击穿

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮

 第五次改造

我在查询缓存的时间,加一把锁synchronized (this) ,这样只会有一个线程进来查询,利用 单利模式的双重检测锁的实现来优化,并放入缓存中,不会有大量的数据进来

    public PatrolTask five(Long productId) {
        PatrolTask patrolTask;
        String productCacheKey = PRODUCT_NAME_KEY + productId;
        String productStr = redisUtil.get(productCacheKey);

        if (C.isNotEmpty(productStr)) {
            //如果查到的值为空,就直接进行返回一个空的对象
            if (PRODUCT_EMPTY.equals(productStr)) {
                return new PatrolTask();
            }
            patrolTask = JSONObject.parseObject(productStr, PatrolTask.class);
            return patrolTask;
        }

        //加锁,只保证有一个线程能够进来
        synchronized (this)  {
             // 这里再去查一次缓存,相当于第二个请求,就会查缓存,用到了单利设计模式
            if (C.isNotEmpty(productStr)) {
                if (PRODUCT_EMPTY.equals(productStr)) {
                    return new PatrolTask();
                }
                patrolTask = JSONObject.parseObject(productStr, PatrolTask.class);
                return patrolTask;
            }

            PatrolTask dbData = iPatrolTaskService.getById(productId);
            if (C.isNotEmpty(dbData)) {

                redisUtil.set(productCacheKey, JSON.toJSON(dbData), getProductCacheTime());
            } else {
               
                redisUtil.set(productCacheKey, PRODUCT_EMPTY, getProductCacheTime());

            }
            return dbData;

        }


    }

以上代码可能会存在的问题:    

synchronized锁事非常重的锁,分布式环境情况下,每个系统都会去重建一次,会造成一定的性能开销,目前这个的性能开销并不大,但是(this)锁的范围非常大!!

会把所有的商品的查询都给锁住,会造成巨大的性能开销。

所以上锁的时候,一定要去考虑锁的范围,范围越小越好

  第五次改造

采用分布式锁Redisson来解决以上的问题,锁的对象能加锁的到产品的对象,这样范围就会很小。

    @Autowired
    private Redisson redisson;

    private final static String product_lock = "product:lock";


    public PatrolTask six(Long productId) {
        PatrolTask patrolTask;
        String productCacheKey = PRODUCT_NAME_KEY + productId;
        String productStr = redisUtil.get(productCacheKey);

        if (C.isNotEmpty(productStr)) {
            //如果查到的值为空,就直接进行返回一个空的对象
            if (PRODUCT_EMPTY.equals(productStr)) {
                return new PatrolTask();
            }
            patrolTask = JSONObject.parseObject(productStr, PatrolTask.class);
            return patrolTask;
        }

        RLock lock = redisson.getLock(product_lock + productId);
        //加锁
        lock.lock(20, TimeUnit.MINUTES);
        try {

            if (C.isNotEmpty(productStr)) {
                if (PRODUCT_EMPTY.equals(productStr)) {
                    return new PatrolTask();
                }
                patrolTask = JSONObject.parseObject(productStr, PatrolTask.class);
                return patrolTask;
            }

            patrolTask = iPatrolTaskService.getById(productId);
            if (C.isNotEmpty(patrolTask)) {

                redisUtil.set(productCacheKey, JSON.toJSON(patrolTask), getProductCacheTime());
            } else {

                redisUtil.set(productCacheKey, PRODUCT_EMPTY, getProductCacheTime());
            }

        } finally {
            //释放锁
            lock.unlock();

        }

        return patrolTask;

    }

以上代码可能会存在的问题:    

如果在我们查到数据的时间,整个网络有延时的情况,就在这个时候,有人更新人这个产品,并已存入数据库,而我们拿到的是旧的信息,并放入了缓存中,那么下次来查询的时候,查询缓存发现有数据,就会直接返回,只有 等缓存过期了才会更新缓存。这就是典型的缓存与数据库不一致问题

   第五次改造

常用的方案:延迟双删、加(Redisson读写锁)以下提供Redisson的读写锁的操作,下面我讲解的是读锁的代码,写的代码,同理在update更新操作的时候,你锁同一个对象,并readWriteLock.writeLock() 获取到读的锁就可以,读写锁不清楚的可自行了解下

    @Autowired
    private Redisson redisson;

    private final static String product_lock = "product:lock";


    public PatrolTask six(Long productId) {
        PatrolTask patrolTask;
        String productCacheKey = PRODUCT_NAME_KEY + productId;
        String productStr = redisUtil.get(productCacheKey);

        if (C.isNotEmpty(productStr)) {
            //如果查到的值为空,就直接进行返回一个空的对象
            if (PRODUCT_EMPTY.equals(productStr)) {
                return new PatrolTask();
            }
            patrolTask = JSONObject.parseObject(productStr, PatrolTask.class);
            return patrolTask;
        }

        RLock lock = redisson.getLock(product_lock + productId);
        //加锁
        lock.lock(20, TimeUnit.MINUTES);
        try {

            if (C.isNotEmpty(productStr)) {
                if (PRODUCT_EMPTY.equals(productStr)) {
                    return new PatrolTask();
                }
                patrolTask = JSONObject.parseObject(productStr, PatrolTask.class);
                return patrolTask;
            }

            RReadWriteLock readWriteLock = redisson.getReadWriteLock(product_lock + productId);
            RLock rLock = readWriteLock.readLock();
            try {
                patrolTask = iPatrolTaskService.getById(productId);
                if (C.isNotEmpty(patrolTask)) {

                    redisUtil.set(productCacheKey, JSON.toJSON(patrolTask), getProductCacheTime());
                } else {

                    redisUtil.set(productCacheKey, PRODUCT_EMPTY, getProductCacheTime());
                }
                
            }   finally {
                rLock.unlock();
            }
         
        } finally {
            //释放锁
            lock.unlock();

        }
        return patrolTask;

    }

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

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

相关文章

2023年12月11日:ui界面跳转

头文件&#xff1a;Second #ifndef SECOND_H #define SECOND_H#include <QWidget>namespace Ui { class Second; }class Second : public QWidget {Q_OBJECTpublic:explicit Second(QWidget *parent nullptr);~Second(); public slots:void jump_slot(); private:Ui::S…

飞桨星河文心SDK与open interpreter构成“小天网”雏形

飞桨星河文心SDK与open interpreter构成“小天网”雏形 开放式解释器open interpreter是大模型和自然语言交互的神器&#xff0c;本项目旨在体验文心大模型为底座的open interpreter。本项目只需使用CPU环境即可运行&#xff0c;直接运行即可“运行全部Cell”&#xff0c;本项…

访问控制列表ACL学习

ACL概念 ACL: ACL 是 Access Control List&#xff08;访问控制列表&#xff09;的缩写。它是一种用于管理和控制访问权限的机制或数据结构。ACL 用于确定谁可以访问特定资源&#xff08;例如文件、文件夹、网络资源等&#xff09;以及他们可以执行的操作。ACL 通常由一系列访…

【EMNLP 2023】面向Stable Diffusion的自动Prompt工程算法

近日&#xff0c;阿里云人工智能平台PAI与华南理工大学朱金辉教授团队合作在自然语言处理顶级会议EMNLP2023上发表了BeautifulPrompt的深度生成模型&#xff0c;可以从简单的图片描述中生成高质量的提示词&#xff0c;从而使文生图模型能够生成更美观的图像。BeautifulPrompt通…

1、混合方式UI设计

1、混合方式UI设计 新建项目添加静态资源添加资源添加action添加菜单菜单栏工具栏中间编辑区域 代码添加其他组件字体和大小状态栏 添加槽函数UI设置的转到槽的手写的设置应用程序图标 代码 新建项目 MainWindow代码文件夹主窗口为 (QMainWindow) 添加静态资源 AppIcon.icoi…

【MATLAB】基于CEEMDAN分解的信号去噪算法(基础版)

代码的使用说明 【MATLAB】基于CEEMDAN分解的信号去噪算法&#xff08;基础版&#xff09; 代码流程图 代码效果图 获取代码请关注MATLAB科研小白的个人公众号&#xff08;即文章下方二维码&#xff09;&#xff0c;并回复CEEMDAN去噪 本公众号致力于解决找代码难&#xff0c;…

【C++】POCO学习总结(十二):流(文本编解码、数据压缩、文件读写流等)

【C】郭老二博文之&#xff1a;C目录 1、说明 POCO提供了多种流类&#xff0c;与标准c IOStreams兼容。 大多数POCO流类被实现为过滤器&#xff0c;这意味着它们不写入或读取设备&#xff0c;而是从它们连接的另一个流。 2、文本编解码 2.1 说明 POCO提供了用于编码和解码…

苹果文本动态高亮,滚动时候部分高亮不显示问题

很简单的需求&#xff1a; 一个文本容器固定大小&#xff0c;内容超出滚动&#xff0c;然后文本点击高亮&#xff0c;奇怪就是苹果微信打开会出现点击只会高亮能看见的区域文本&#xff0c;滚动部分不会显示&#xff0c;默认浏览器打开也不行&#xff0c;安卓没问题&#xff0…

10.RIP路由信息协议

10.RIP 网段经常产生变化的话&#xff0c;建议使用动态路由协议&#xff0c;当网段发生变化的时候会自动通告给其他路由器 它不看链路的带宽&#xff0c;只看链路中的跳数&#xff0c;只要是跳数多的&#xff0c;不管带宽有多大&#xff0c;它就认为是不好的 RIP跳数有限 …

外包干了3个月,技术退步明显。。。

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

第三节、项目支付功能实战-微信支付平台接入流程,小程序账号注册、商户注册

简介 本篇介绍小程序的注册流程、商户平台的注册流程、以及小程序和商户平台如何进行绑定。 微信小程序注册 由于项目中使用了小程序进行支付&#xff0c;所以首先来注册小程序。小程序注册网站如下&#xff1a;小程序注册地址 小程序账号注册 1、链接页面点击“前往注册”…

事务的四个特性、四个隔离级别以及数据库的常用锁

事务的四个特性、四个隔离级别以及数据库的常用锁 四大特性 事务的四大特性&#xff0c;通常被称为ACID特性&#xff0c;是数据库管理系统&#xff08;DBMS&#xff09;确保事务处理的关键属性。这四大特性分别是&#xff1a; 原子性&#xff08;Atomicity&#xff09;&#x…

银河麒麟v10系统SSH远程管理及切换root用户的操作方法

&#x1f4da;&#x1f4da; &#x1f3c5;我是默&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; ​​ &#x1f31f;在这里&#xff0c;我要推荐给大家我的专栏《Linux》。&#x1f3af;&#x1f3af; &#x1f680;无论你是编程小白&#xff0c;还是有一…

Python基础期末复习 新手 2

虽然age 10在__init__方法中定义了一个局部变量age&#xff0c;但这个局部变量并不会影响类属性age的值。类属性是在类级别上定义的&#xff0c;不属于任何一个实例。因此&#xff0c;在创建实例s1和s2时&#xff0c;它们的age属性值都为类属性的初始值0。 尽管对类的属性值进…

1.鸿蒙应用程序开发app_hap开发环境搭建

1.下载Node.js, Javascipts的运行环境 node.js版本下载v12.18.3/https://www.cnblogs.com/txwtech/p/17865780.html 2.下载并安装DevEco Studio DevEco Studio 3.1 DevEco Studio 3.1配套支持HarmonyOS 3.1版本及以上的应用及服务开发&#xff0c;提供了代码智能编辑、低代…

coding创建远程分支。并拉取远程新分支+推送代码

进入coding ----项目----代码仓库---点击 下拉之后查看全部----创建分支 创建分支之后执行下面命令 git branch -a // 查看所有分支 这个时候发现自己创建的分支没有显示这是因为自己在远程创建了分支但是本地还没有分支 执行 git fetch命令 用于从远程仓库获取最新的提交…

day33-37-SpringBootV12(整合Spring,SpringMVC,Mybatis,日志,api测试等框架)

ssm spring --> applicationContext.xml配置文件 springmvc --> springmvc.xml配置文件 mybatis —> mybatis-config.xml配置文件 —> springboot优化了之前的框架配置,思想是约定大于配置 一、引言 1.1 初始化配置 为了使用SSM框架去开发&#xff0c;准备SSM…

SpringBootWeb请求响应之前言及状态码的详细解析

SpringBootWeb请求响应 前言 在上一次的课程中&#xff0c;我们开发了springbootweb的入门程序。 基于SpringBoot的方式开发一个web应用&#xff0c;浏览器发起请求 /hello 后 &#xff0c;给浏览器返回字符串 “Hello World ~”。 其实呢&#xff0c;是我们在浏览器发起请求…

孩子还是有一颗网安梦——Bandit通关教程:Level0

&#x1f575;️‍♂️ 专栏《解密游戏-Bandit》 &#x1f310; 游戏官网&#xff1a; Bandit游戏 &#x1f3ae; 游戏简介&#xff1a; Bandit游戏专为网络安全初学者设计&#xff0c;通过一系列级别挑战玩家&#xff0c;从Level0开始&#xff0c;逐步学习基础命令行和安全概念…

JAVA定时任务技术总结

在日常的项目开发中&#xff0c;多多少少都会涉及到一些定时任务的需求。例如每分钟扫描超时支付的订单&#xff0c;每小时清理一次数据库历史数据&#xff0c;每天统计前一天的数据并生成报表&#xff0c;定时去扫描某个表的异常信息&#xff08;最终一致性的方案也可能涉及&a…