【Redis】缓存击穿问题及其解决方案

news2024/11/20 13:34:47

【Redis】缓存击穿问题及其解决方案

文章目录

  • 【Redis】缓存击穿问题及其解决方案
    • 1. 缓存击穿概念
    • 2. 解决方案
      • 2.1 互斥锁
        • 2.1.1 互斥锁的优缺点
        • 2.1.2 互斥锁的代码实现
      • 2.2 逻辑过期
        • 2.2.1 逻辑过期的优缺点
        • 2.2.2 逻辑过期的代码实现

1. 缓存击穿概念

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

如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IMqkJoHL-1673507243265)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230111162127504.png)]

线程1缓存未命中,去重建缓存;在线程1重建缓存的时候,线程2缓存又没命中,线程2也去重建缓存;和线程2同时来的线程3,线程4…缓存都没命中,都去重建缓存,给数据库带来了巨大的压力。


2. 解决方案

缓存击穿的常见解决方案有两种:

  • 互斥锁
  • 逻辑过期

2.1 互斥锁

互斥锁的实现思路就是在第一个线程到来的时候获取互斥锁,后面的线程来到之后尝试去获取互斥锁,获取失败,于是进行休眠重试。直到第一个线程缓存重建成功之后,释放互斥锁。之后其余线程在重试过程中就成功查询缓存命中了重建数据。

互斥锁的流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-azeAoiqm-1673507243266)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230111162937081.png)]


2.1.1 互斥锁的优缺点

优点:

  • 没有额外的内存消耗
  • 保证一致性(数据库和redis数据一致)
  • 实现简单

缺点:

  • 线程需要等待,性能受影响
  • 可能有死锁风险(一个方法里有多个查询操作,另一个方法也有多个重合的查询操作)

2.1.2 互斥锁的代码实现

我们先设定一个场景:假设这是一个电商平台,我们通过id去查询店铺信息。

代码实现流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QJe2Cbyn-1673507243267)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230112102744196.png)]

首先我们编写获取锁和释放锁的方法,如下所示:

//获取锁
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

//释放锁
private void unLock(String key) {
    stringRedisTemplate.delete(key);
}

然后编写一个解决缓存击穿问题的方法,最后写一个调用解决方法的业务方法:

@Override
public Result queryById(Long id) {
    //缓存空对象解决 缓存穿透
    //Shop shop = queryWithPassThrough(id);

    //互斥锁解决 缓存击穿
    Shop shop = queryWithMutex(id);
    if (shop == null) {
        return Result.fail("店铺不存在!");
    }
    return Result.ok(shop);
}

public Shop queryWithMutex(Long id) {
    //1.从redis查询商铺缓存
    String key = CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        //3.存在,直接返回
        return JSONUtil.toBean(shopJson, Shop.class);
    }
    //此时 shopJson 不是为null就是为""
    if (shopJson != null) {
        //为""直接返回错误信息,为null查询数据库
        return null;
    }

    //4.实现缓存重建
    //4.1.获取互斥锁
    String lockKey = "lock:shop:" + id;
    Shop shop = null;
    try {
        boolean isLock = tryLock(lockKey);
        //4.2.判断是否获取成功
        while (!isLock) {
            //4.3.失败,则休眠重试
            Thread.sleep(50);
            return queryWithMutex(id);
        }
        //4.4.获取锁成功,再次检测缓存释放存在(double check)
        String cacheShopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(cacheShopJson)) {
            //4.5.存在,直接返回
            return JSONUtil.toBean(cacheShopJson, Shop.class);
        }
        //5.缓存数据不存在,根据id查询数据库
        shop = getById(id);
        //模拟重建的延时
        Thread.sleep(200);
        //6.不存在,返回错误
        if (shop == null) {
            //缓存空值
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //7.存在,写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        //8.释放锁
        unLock(lockKey);
    }
    return shop;
}

2.2 逻辑过期

逻辑过期就是给缓存的数据添加一个逻辑过期字段,而不是真正的给它设置一个TTL。每次查询缓存的时候去判断是否已经超过了我们设置的逻辑过期时间,如果未过期,直接返回缓存数据;如果已经过期则进行缓存重建。

逻辑过期的流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O7g61ksR-1673507243267)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230111163351906.png)]

解释:第一个线程到来之后发现逻辑过期,于是获取互斥锁,再开启一个新线程去进行缓存重建。当后续线程到来时,发现缓存已过期,尝试获取互斥锁也失败,但是此时不进行等待重试,而是直接返回过期数据。之后第一个线程成功缓存数据释放互斥锁之后,后面线程继续来访,发现命中缓存并且没有过期,返回重建数据。


2.2.1 逻辑过期的优缺点

优点:

  • 线程无需等待,性能较好

缺点:

  • 不保证一致性(因为会返回过期数据)
  • 有额外的内存消耗(同时缓存了逻辑过期时间的字段)
  • 实现复杂

2.2.2 逻辑过期的代码实现

我们先设定一个场景:假设这是一个电商平台,我们通过id去查询店铺信息。

代码实现流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MLB2j8pP-1673507243268)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230112143640211.png)]

1)构建存储类

我们想要实现逻辑过期,首先得清楚redis中到底要存储什么样的数据?我们是不是要在每个类中都添加一个逻辑过期的字段?这是不对的,如果我们再每个类中都添加了一个逻辑过期时间字段,这样对原代码就有了 侵入性 ,我们应该使整个系统具有可拓展性,所以我们应该新建一个类来填充要存入redis的数据,代码如下:

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

2)创建线程池

由于我们需要开启独立线程去重建缓存,所以我们可以选择创建一个线程池。

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

3)编写缓存重建的代码

缓存重建就是直接查询数据库,将查询到的数据缓存到redis中。

public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
    //1.查询店铺数据
    Shop shop = getById(id);
    //2.封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    //设置逻辑过期时间
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

4)编写业务方法并调用缓存击穿方法

@Override
public Result queryById(Long id) {
    //缓存空对象解决 缓存穿透
    //Shop shop = queryWithPassThrough(id);

    //互斥锁解决 缓存击穿
    //Shop shop = queryWithMutex(id);

    //逻辑过期解决 缓存击穿
    Shop shop = queryWithLogicalExpire(id);
    if (shop == null) {
        return Result.fail("店铺不存在!");
    }
    return Result.ok(shop);
}

public Shop queryWithLogicalExpire(Long id) {
    //1.从redis查询商铺缓存
    String key = CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //2.判断是否存在
    if (StrUtil.isBlank(shopJson)) {
        //未命中,直接返回空
        return null;
    }
    //3.命中,判断是否过期
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    Shop cacheShop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
        //3.1未过期,直接返回店铺信息
        return cacheShop;
    }
    //3.2.已过期,缓存重建
    //3.3.获取锁
    String lockKey = LOCK_SHOP_KEY + id;
    boolean flag = tryLock(lockKey);
    if (flag) {
        //3.4.获取成功
        //4再次检查redis缓存是否过期,做double check
        shopJson = stringRedisTemplate.opsForValue().get(key);
        //4.1.判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            //未命中,直接返回空
            return null;
        }
        //4.2.命中,判断是否过期
        redisData = JSONUtil.toBean(shopJson, RedisData.class);
        cacheShop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            //4.3.未过期,直接返回店铺信息
            return cacheShop;
        }
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            //5.重建缓存
            try {
                this.saveShop2Redis(id, 20L);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                unLock(lockKey);
            }
        });
    }
    //7.获取失败,返回旧数据
    return cacheShop;
}

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

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

相关文章

13个有趣的Python高级脚本,建议收藏

上一篇文章:整理了上千个 Python 工具库,涵盖24个大方向 没想到火了,喜欢除了收藏外,记得点赞。 每天我们都会面临许多需要高级编码的编程挑战。你不能用简单的 Python 基本语法来解决这些问题。 在本文中,我将分享…

Python针对列表进行去重、排序、大小写转换

首先我们要进行的就是列表去重 Python针对列表进行去重、排序、大小写转换1. 针对列表进行去重第一种方法是用for循环第二种方法就是列式推导法第三种方法就是set第四种方法就是用字典去重2.进行大小写转换大写转小写使用lower进行小写转换列式推导法swapcase()将字符串内的大写…

【windows环境使用gcc完美编译C/C++】

windows环境使用gcc完美编译C/C前言安装最新版的msys2安装mingw版的gcc前言 在windows使用gcc编译c,很多文章会使用Cygwin或者msys来安装gcc,并编译本地c,但编译好的.exe执行文件在其他电脑上会报找不到cygwin1.dll或找不到msys-2.0.dll的错误…

基于JavaSpringboot+vue国风汉服文化交流宣传系统

博主介绍:✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末底部获取联系🍅 精彩专栏推荐订阅收藏👇…

ArcgisPro3.0及3.0.1及3.0.2安装教程

一、获取: https://www.xsoftnet.com/share/a0XohtwU8Zz.html二、产品介绍:ArcgisPro3.0-3.0.1中文安装包下载及安装教程 ArcGisPro3.x安装教程 一、安装包结构介绍:可能你下载的安装包目录结构有所不同但是核心文件是一样的。 安装顺序&…

【SpringCloud07】SpringCloud 整合Zookeeper注册中心

1.Zookeeper注册中心 zookeeper是一个分布式协调工具&#xff0c;可以实现注册中心功能关闭Linux服务器防火墙启动zookeeper服务器zookeeper服务器取代Eureka服务器&#xff0c;zookeeper作为服务注册中心 2.服务提供者 1.新建cloud-provider-payment8004 2.POM <?xm…

(小甲鱼python)函数笔记合集二 函数(II)总结 函数的几种参数 位置参数、关键字参数、默认参数 .join()函数的用法等

一、基础复习 函数的基本用法 创建和调用函数 函数的形参与实参等等 二、函数的几种参数 1.位置参数 一般情况下实参是按照形参定义的顺序进行传递的&#xff0c;而Python中位置固定的参数我们称为位置参数。 >>> def myfunc(s,vt,o):return "".join((o…

低成本MEMS INS系统 + GNSS组合导航MATLAB仿真

感谢西工大严老师的无私奉献&#xff01;&#xff01; 低成本MEMS INS系统 GNSS组合导航MATLAB仿真感谢西工大严老师的无私奉献&#xff01;&#xff01;一、kalman参数初始化——kfinit()二、imu添加误差——imusdderr()三、imu位姿更新——insupdate()四、kalman误差方程——…

【1】K8s的组件及概念

目录 1、K8s的组件 2、组件概念 1、K8s的组件 一个kubernetes集群主要是由控制节点(master)、工作节点(node)构成&#xff0c;每个节点上都会安装不同的组件。 master: 集群的控制平面&#xff0c;负责集群的决策 ApiServer:资源操作的唯一入口&#xff0c;接收用户输入的命令…

ArcGIS基础实验操作100例--实验92以图形与表格构建趋势面

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 空间分析篇--实验92 以图形与表格构建趋势面 目录 一、实验背景 二、实验数据 三、实验步骤 &#xf…

ts实现简易观察者模式

什么是观察者模式 观察者模式能让你时刻知悉对象状态的变化的一种设计模式,是一种一对多依赖的关系,比如报纸的订阅 生活中随处可见的观察者模式(猎头与求职者): headfirst设计模式气象站案例 通知更新的方式有两种: 主题推给观察者和观察者自己去主题拉取两种方式,大部分实现…

年中盘点 | 2022年,PaaS 再升级

过去十五年&#xff0c;是云计算从无到有突飞猛进的十五年。PaaS 作为云计算的重要组成部分&#xff0c;在伴随着云计算高速发展的同时&#xff0c;在云计算产业链中的关键性作用日渐凸显。关于 PaaS&#xff0c;很多人都认同一个观点&#xff0c;在公有云上&#xff0c;除了 I…

【寒假每日一题】AcWing 4699. 如此编码

目录 一、题目 1、原题链接 2、题目描述 二、解题报告 1、思路分析 2、时间复杂度 3、代码详解 一、题目 1、原题链接 4699. 如此编码 - AcWing题库 2、题目描述 某次测验后&#xff0c;顿顿老师在黑板上留下了一串数字 23333便飘然而去。 凝望着这个神秘数字&#xff…

雄关漫道真如铁,而今迈步从头越 | 挥别2022,再战2023!

挥别2022年 这一年&#xff0c;虽面临诸多挑战&#xff0c;但我们充满干劲儿 向下扎根&#xff0c;向上生长 这一年&#xff0c;我们风云十载&#xff0c;厚积薄发 站在2023年的开端 让我们一起回顾博云2022年的这些成绩 No.1 专精特新&#xff0c;示范引领 2022年8月&am…

【鸟哥杂谈】Linux环境搭建Redis

忘记过去&#xff0c;超越自己 ❤️ 博客主页 单片机菜鸟哥&#xff0c;一个野生非专业硬件IOT爱好者 ❤️❤️ 本篇创建记录 2023-01-12 ❤️❤️ 本篇更新记录 2023-01-12 ❤️&#x1f389; 欢迎关注 &#x1f50e;点赞 &#x1f44d;收藏 ⭐️留言&#x1f4dd;&#x1f64…

指针笔试题详细介绍,让你不再惧怕指针【c语言】

int main() {int a[5] { 1, 2, 3, 4, 5 };int *ptr (int *)(&a 1); //&a1的类型是int&#xff08; * &#xff09;[5]printf( "%d,%d", *(a 1), *(ptr - 1));//输出2 5 return 0; }&a&#xff0c; 取出整个数组的地址&#xff0c;放在一个数组指针中…

你拿了多少年终奖?

见字如面&#xff0c;我是军哥&#xff01;前几天我看到一个大 V 调研他的程序员粉丝&#xff0c;都拿了多少年终奖&#xff1f;结果可想而知&#xff0c;2000 多人参与问卷调查&#xff0c;53% 左右的人说今年没有年终奖。另外&#xff0c;我估计大多数人今年的年终奖金额相比…

超能面板PRO搭载北京君正研发的X2000多核异构跨界处理器

每一座建筑&#xff0c;都承载着它独特的生活方式。隐匿在老胡同里的四合院&#xff0c;见证了大院三代同堂的喧闹欢愉&#xff0c;散落在烟雨中的园林小院散发着对诗意生活的淡然向往。在一代又一代的变迁中&#xff0c;逐渐形成了符合居住者气质的生活方式。历史与现代在不断…

等差素数数列

问题描述&#xff1a; 2,3,5,7,11,13,…是素数序列。 类似&#xff1a;7,37,67,97,127,157 这样完全由素数组成的等差数列&#xff0c;叫等差素数数列。 上边的数列公差为30&#xff0c;长度为6。 2004年&#xff0c;格林与华人陶哲轩合作证明了&#xff1a;存在任意长度的素数…

事件总线 + 函数计算构建云上最佳事件驱动架构应用

作者 | 史明伟&#xff08;世如&#xff09; 距离阿里云事件总线&#xff08;EventBridge&#xff09;和 Serverless 函数计算&#xff08;Function Compute&#xff0c;FC&#xff09;宣布全面深度集成已经过去一年。站在系统元数据互通&#xff0c;产品深度集成的肩膀上&…