【Redis】分别从互斥锁与逻辑过期两个方面来解决缓存击穿问题

news2025/1/21 8:50:20

文章目录

  • 前言
    • 一.什么是缓存击穿
    • 二.基于互斥锁解决缓存击穿
    • 三.基于逻辑过期解决缓存击穿
    • 四.接口测试
    • 五.两者对比

前言

身逢乱世,未雨绸缪

一.什么是缓存击穿

说直白点,就是一个被非常频繁使用的key突然失效了请求没命中缓存,而因此造成了无数的请求落到数据库上,瞬间将数据库拖垮。而这样的key也被叫做热key!

在这里插入图片描述
可以直观地看到,要想解决缓存击穿绝对不能让这么多线程的请求在某一时段大量去访问到数据库。
以此为基础,针对访问数据库的限制有两种解决方案:

二.基于互斥锁解决缓存击穿

对于一个访问频繁的id查询接口,可能会发生缓存击穿问题,下面通过互斥锁的方式来解决
在这里插入图片描述
在以前,id查询信息的接口里一般将查询的信息写到缓存里,针对是否命中缓存再去做对应的处理。而在并发的情况下,对于热Key失效的情况,大量的请求则会直接打到数据库上并试图重建缓存,很有可能打停数据库,导致服务中断。对于这样的情况往往是在未命中缓存时,最佳的处理点就在于业务中判断缓存是否命中之后的那一步操作,即“多余”的请求对数据库的访问与否。
其他线程的请求能不能去访问数据库?什么时候才能去访问数据库?
其他的线程能不能去访问数据库?——加锁,有锁才能
什么时候才能去访问数据库?——等主线程释放锁
那其他线程拿不到锁的时间该干嘛?——睡吧,等会再来

为了实现在多个线程并行的情况下只能有一个线程获得锁,我们可以使用Redis自带的setnx
在这里插入图片描述
他可以保证在key不存在时可以进行写的操作,key存在时无法进行写的操作,这就完美地保证了在并发情况下只有第一个拿到锁的线程才能去写,并且他写完了之后(在不释放的前提下)别人就写不了了。
如何去获取?写个Key—Value进去
如何释放?把Key删了 del lock (通常设置一个有效期,避免长时间未释放的情况)

这样我就可以以此为条件封装两个方法,一个写key来尝试获取锁另一个删key来释放锁。就像这样:

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

/**
 * 释放锁
 *
 * @param key
 */
private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

在并行情况下每当其他线程想要获取锁,来访问缓存都要通过将自己的key写到tryLock()方法里,setIfAbsent()返回false则说明有线程在在更新缓存数据,锁未释放。若返回true则说明当前线程拿到锁了可以访问缓存甚至操作缓存。
我们在下面一个热门的查询场景中用代码用代码来实现互斥锁解决缓存击穿
在这里插入图片描述

    /**
     * 解决缓存击穿的互斥锁
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1.从Redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);  //JSON格式
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) { //不为空就返回 此工具类API会判断""为false
            //存在则直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            //return Result.ok(shop);
            return shop;
        }
        //3.判断是否为空值
        if (shopJson != null) {
            //返回一个空值
            return null;
        }
        //4.缓存重建
        //4.1获得互斥锁
        String lockKey = "lock:shop"+id;
        Shop shopById=null;
        try {
            boolean isLock = tryLock(lockKey);
            //4.2判断是否获取成功
            if (!isLock){
                //4.3失败,则休眠并重试
                Thread.sleep(50);
               return queryWithMutex(id);
            }
            //4.4成功,根据id查询数据库
            shopById = getById(id);
            //5.不存在则返回错误
            if (shopById == null) {
                //将空值写入Redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                //return Result.fail("暂无该商铺信息");
                return null;
            }
            //6.存在,写入Redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //7.释放互斥锁
            unlock(lockKey);
        }

        return shopById;
    }

三.基于逻辑过期解决缓存击穿

逻辑过期不是真正的过期,对于对应的Key我们并不需要去设置TTL,而是通过业务逻辑来达到一个类似于“过期”的效果。其本质还是限制落到数据库的请求数量!但前提是牺牲一致性保证可用性,还是上一个业务的接口,通过使用逻辑过期来解决缓存击穿:
在这里插入图片描述
这样一来,缓存基本是会被命中的,因为我没有给缓存设置任何过期时间,并且对于Key的set都是事先选择好的,如果出现未命中的情况基本可以判断他不在选择之内,这样我就可以直接返回错误信息。那么对于命中的情况,就需要先判断逻辑时间是否过期,根据结果再来决定是否进行缓存重建。而这里的逻辑时间就是减少大量请求落到数据库的一个“关口”

看完上面这一段,相信大家还很迷惑。既然没有设置过期时间,那你为什么还要判断逻辑过期时间,怎么还存在过不过期的问题?
其实,这里所谓的逻辑过期时间只是一个类的属性字段,根本没有上升到Redis,上升到缓存的层面,是用来辅助判断查询对象的,也就是说,所谓的过期时间与缓存数据是剥离开的,所以根本不存在缓存过期的问题,自然数据库也不会有压力。

代码阶段:

为了尽可能地贴合开闭原则,不采用继承的方式来扩展原实体的属性而是通过组合的形式。

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;  //这里用Object是因为以后可能还要缓存别的数据
}

封装一个方法用来模拟更新逻辑过期时间与缓存的数据在测试类里运行起来达到数据与热的效果

/**
 * 添加逻辑过期时间
 *
 * @param id
 * @param expireTime
 */
public void saveShopRedis(Long id, Long expireTime) {
    //查询店铺信息
    Shop shop = getById(id);
    //封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
    //将封装过期时间和商铺数据的对象写入Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

查询接口:

/**
 * 逻辑过期解决缓存击穿
 *
 * @param id
 * @return
 */
public Shop queryWithLogicalExpire(Long id) throws InterruptedException {
    String key = CACHE_SHOP_KEY + id;
    Thread.sleep(200);
    //1.从Redis查询缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);  //JSON格式
    //2.判断是否存在
    if (StrUtil.isBlank(shopJson)) {
        //不存在则直接返回
        return null;
    }
    //3.判断是否为空值
    if (shopJson != null) {
        //返回一个空值
        //return Result.fail("店铺不存在!");
        return null;
    }
    //4.命中
    //4.1将JSON反序列化为对象
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    //4.2判断是否过期
    if (expireTime.isAfter(LocalDateTime.now())) {
        //5.未过期则返回店铺信息
        return shop;
    }
    //6.过期则缓存重建
    //6.1获取互斥锁
    String LockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(LockKey);
    //6.2判断是否成功获得锁
    if (isLock) {
        //6.3成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                //重建缓存
                this.saveShop2Redis(id, 20L);

            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                unlock(LockKey);
            }
        });
    }
    //6.4返回商铺信息
    return shop;
}

四.接口测试

可以看到通过APIfox模拟并发场景进行接口测试,平均耗时还是很短的,控制台的日志也没有频繁的去访问数据库的记录:
在这里插入图片描述
由于ApiFox不支持大量线程,我又用jmeter拿1550个线程测试了一下,接口依然都可以跑通!
在这里插入图片描述
看来接口在并发场景下性能还不错,QPS也挺理想

五.两者对比

可以看到,互斥锁的方式代码层面更加简单,只需要封装两个简单的方法来操作锁。而逻辑过期的方式更加复杂,需要额外增添实体类,封装方法之后还要去测试类里模拟数据预热。
相比之下,前者没有消耗额外的内存(不开新线程),数据一致性强,但是线程需要等待,性能可能不好并且有死锁的风险。后者开辟了新的线程有额外的内存消耗,牺牲一致性保证可用性,但是不要需等待性能比较好。

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

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

相关文章

使用electron将vue项目打包成exe

文章目录一、前言二、实现方法1.跑通示例代码 electron-quick-start<1>clone示例代码<2>进入项目根目录&#xff0c;下载依赖<3>测试运行2.打包自己的 vue 项目3.将vue项目整合到示例代码中打包exe<1>将打包好的 dist 文件夹复制到示例代码 electron-q…

sklearn之OPTICS聚类

文章目录简介sklearn实现cluster_optics_dbscan简介 OPTICS算法&#xff0c;全称是Ordering points to identify the clustering structure&#xff0c;是一种基于密度的聚类算法&#xff0c;是DBSCAN算法的一种改进。 众所周知&#xff0c;DBSCAN算法将数据点分为三类&#…

ResNet精读(2)

FLOPs &#xff1a;整个网络要计算多少个浮点运算 卷积层的浮点运算等价于 输入的高*输入的宽*通道数*输出通道数再乘以卷积核的高和宽再加上全连接的一层 我们发现训练的时候的精度是要比测试精度来的高的在一开始&#xff0c;这是因为训练的时候用了数据增强 使得训练误差…

2022年莱佛士大盘点 ,设计的种子遍地开花!

2022似乎过得尤其之快&#xff0c;反复的居家隔离和线上网课&#xff0c;似乎给2022蒙上了一层雾蒙蒙的灰色。但2022总还给我们留下了些东西&#xff0c;在莱佛士设计学院&#xff0c;我们共同见证了梦想的种子在设计的各个领域遍地开花。现在我们一起来看看2022年莱佛士学生们…

广义表——LISP的基石

线性表中存放的是同一类型的元素&#xff0c;而广义表是线性表的推广&#xff0c;即广义表中除包含类型相同的元素外&#xff0c;还可以包含具有其自身结构的元素。在人工智能领域使用十分广泛的 LISP语言中&#xff0c;广义表是一种基本数据类型&#xff0c;LISP 语言中的数据…

Vue3案例-todoMVC-pinia版 (可跟做练手)

列表展示功能 &#xff08;1&#xff09; 在main.js中引入pinia import { createApp } from vue import App from ./App.vue import { createPinia } from pinia import ./styles/base.css import ./styles/index.cssconst pinia createPinia() createApp(App).use(pinia).m…

Spring源码学习~11、Bean 的加载步骤详解(二)

Bean 的加载步骤详解&#xff08;二&#xff09; 一、循环依赖 1、什么是循环依赖 循环依赖就是循环引用&#xff0c;即两个或多个 bean 互相之间持有对方&#xff0c;如下图&#xff1a; 循环引用不是循环调用&#xff0c;循环调用是方法之间的环调用&#xff0c;循环调用是…

谷粒学院——Day18【权限管理Spring Security、配置中心Nacos、代码托管git】

❤ 作者主页&#xff1a;欢迎来到我的技术博客&#x1f60e; ❀ 个人介绍&#xff1a;大家好&#xff0c;本人热衷于Java后端开发&#xff0c;欢迎来交流学习哦&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 如果文章对您有帮助&#xff0c;记得关注、点赞、收藏、…

Linux搭建Gitlab保姆级教程

文章目录1、gitlab安装1.1、gitlab介绍1.1.1、概念1.1.2、gitlab与github的区别1.1.3、gitlab的优势1.1.4、gitlab主要服务构成1.1.5、gitlab的工作流程1.2、准备工作1.3、安装1.4、配置1.5、启动1.6、测试2、gitlab安装目录3、gitlab常用命令4、注册账号5、gitlab相关设置5.1、…

上半年要写的博客文章23

上半年要写的博客文章21 这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个…

ArcGIS基础实验操作100例--实验76按格网统计点要素

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验76 按格网统计点要素 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&…

java EE 初阶 — CAS 的介绍

文章目录CAS1. 什么是 CAS2. CAS 是怎么实现的3. CAS 有哪些应用3.1 实现原子类3.2 实现自旋锁4. CAS 的 ABA 问题4.1 什么是 ABA 问题4.2 ABA 问题引来的 BUG4.3 解决方案5. 相关面试题CAS 1. 什么是 CAS CAS&#xff1a;全称 Compare and swap&#xff0c;字面意思&#xff…

设计模式——工厂方法模式

文章目录1. 工厂方法模式的定义2. 工厂方法模式的类图3. 工厂方法模式的作用4. 工厂方法模式的实现1. 工厂方法模式的定义 定义了一个创建对象的接口&#xff0c;但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。 2. 工厂方法模式的类图 3. 工厂方法模式…

[教程]一文搞懂STM32使用DHT11采集温湿度

1、DHT11简介 DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器。它应用专用的数字模块采集技术和温湿度传感技术&#xff0c;确保产品具有极高 的可靠性与卓越的长期稳定性。传感器包括一个电阻式感湿元件和一个NTC测 温元件&#xff0c;并与一个高性能8…

GO语言基础-08-内建函数-make()、new()

文章目录1. make1.1 概述1.2 示例&#xff08;make切片&#xff09;1.3 示例&#xff08;make map&#xff09;1.4 示例&#xff08;make 通道&#xff09;2. new2.1 概念2.2 示例&#xff08;new 切片&#xff09;2.3 示例&#xff08;new和make对比&#xff09;2.4 示例&…

Java基础算法每日5道详解(2)

83. Remove Duplicates from Sorted List 从排序列表中删除重复项 Given the head of a sorted linked list, delete all duplicates such that each element appears only once. Return the linked list sorted as well. Example 1: Input: head [1,1,2] Output: [1,2]Exa…

20230109测试ToyBrick的RK3588开发板运行Buildroot的V20220811版本

20230109测试ToyBrick的RK3588开发板运行Buildroot的V20220811版本 2023/1/9 14:25 开发板&#xff1a;Toybrick的TB-RK3588X开发板 SDK&#xff1a;RK3588_LINUX_20220811\rk3588-linux-20220811.tar.gz_06 H:\BaiduNetdiskDownload\RK3588_LINUX_20220811 rk3588-linux-2022…

【SQLyog错误号码2058解决办法】

当你遇到下图这个错误时&#xff0c;是由于SQLyog在8.0以上版本采用了新的加密方式。 解决办法&#xff1a; win R打开 &#xff0c; 输入cmd&#xff0c;打开命令行窗口&#xff0c; 然后连接你的SQLyog版本的服务器&#xff0c; mysql -uroot -P3306 -p注意&#xff1a;…

【Kotlin】数字类型 ( 安全转换函数 | 浮点型转整型 )

文章目录一、安全转换函数二、浮点型转整型一、安全转换函数 在 Kotlin 中 , 将 字符串 String 类型 转为 数字类型 , 如果 字符串 代表的数字类型 与 要换转的 数字类型 不匹配 , 就会出异常 ; 如 : 执行如下代码 , 就会报异常 ; 字符串内容是 0.5 , 显然是一个 Double 类…

Kotlin Flow响应式编程,StateFlow和SharedFlow

本文同步发表于我的微信公众号&#xff0c;扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注&#xff0c;每个工作日都有文章更新。 大家好&#xff0c;今天是Kotlin Flow响应式编程三部曲的最后一篇。 其实回想一下我写这个Kotlin Flow三部曲的初衷&#xff0c;主要还是因为…