Redis:原理速成+项目实战——Redis实战5(互斥锁、逻辑过期解决缓存击穿问题)

news2024/11/24 7:29:18

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理速成+项目实战——Redis实战4(解决Redis缓存穿透、雪崩、击穿)
📚订阅专栏:Redis:原理速成+项目实战
希望文章对你们有所帮助

上一篇文章讲解了缓存击穿问题,以及解决缓存击穿问题的2种解决思路,即互斥锁与逻辑过期,这里将分别用这两种方式解决缓存击穿问题。

互斥锁、逻辑过期解决缓存击穿问题

  • 互斥锁解决缓存击穿
    • 获取锁与释放锁方法封装
    • 业务逻辑修改
    • 测试
  • 逻辑过期解决缓存击穿
    • 代码实现
    • 测试

互斥锁解决缓存击穿

根据上次讲解的互斥锁解决缓存击穿问题的方式,我们可以将客户端查询数据的流程修改为如下:
在这里插入图片描述
这里有比较关键的点,这里的锁并不是我们平常用的锁,平常的锁如果没有获取到的话就一直等待,而这边我们需要自定义逻辑。
我们该如何自定义获取锁跟释放锁的逻辑?我们可以利用Redis的一些数据结构的特性。
我们可以联想到之前学习Redis数据结构的时候,用到的setnx,当且仅当key不存在的时候,setnx才会执行set操作:
在这里插入图片描述
可以看到,当我们的key已经是存在的情况下,我们用setnx指令无法对其value进行修改。所以如果有无数线程进行setnx操作的时候,只有第一个进行操作的线程可以写入value,其他线程都会失败。
所以,这就满足了我们所说的互斥锁的实现方式,其实这也是分布式锁的一个基本原理(当然真正的分布式锁还是比较复杂的)。
而释放锁只需要执行del操作即可,这样其他线程就可以获取到这个锁了:
在这里插入图片描述
当然了,最好别把锁永远加在那,因为如果加了锁的线程,他的工作没办法完成,他就永远没办法释放锁,因此我们在执行setnx操作的时候需要设置一下有效期。

获取锁与释放锁方法封装

进行业务逻辑编写之前,我们可以先把获取锁与释放锁的方法给自定义一下:

private boolean tryLock(String key){//尝试获取锁
    //opsForValue里面没有真正的setnx,而是setIfAbsent,表示如果不存在就执行set
    //值就随便设定一下,重点是要获取到锁,但是设定了TTL为10s
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    /**
     * 如果是直接返回flag,可能会有拆箱操作,造成空指针,需要用BooleanUtil工具类
     * 因为Boolean不是基本类型的boolean,是boolean的封装类
     */
    return BooleanUtil.isTrue(flag);
}

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

业务逻辑修改

我们可以先把之前会出现缓存穿透的代码给封装起来,封装成返回值为Shop类型的,防止代码丢失(之后章节的代码优化会解决缓存穿透问题):

public Shop queryWithPassThrough(Long id){
    String key = CACHE_SHOP_KEY + id;
    //从Redis中查询商铺缓存,存储对象可以用String或者Hash,这里用String
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        //存在,直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return shop;
    }
    //判断命中的是否是null
    if (shopJson != null){
        return null;
    }
    //不存在,根据id查询数据库
    Shop shop = getById(id);
    if (shop == null){
        //存一个null到Redis中
        //这种没用的信息,TTL没必要设置太长了,这里我设置成了2min
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        return null;
    }
    //存在,写入Redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    //返回
    return shop;
}

同样的,我们也可以把互斥锁解决缓存击穿的代码给单独封装起来,注意,我们不管获得互斥锁的线程有没有正常执行,即便执行不成功,也要在最后进行unlock,因此我们要利用try…catch…finally:

public Shop queryWithMutex(Long id){
    String key = CACHE_SHOP_KEY + id;
    //从Redis中查询商铺缓存,存储对象可以用String或者Hash,这里用String
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        //存在,直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return shop;
    }
    //判断命中的是否是null
    if (shopJson != null){
        //返回错误信息
        return null;
    }
    //未命中,开始实现缓存重建
    //尝试获取互斥锁,锁的key跟缓存的key不一样
    String lockKey = "lock:shop:" + id;
    Shop shop = null;
    try {
        boolean isLock = tryLock(lockKey);
        //判断是否获取成功
        if (!isLock){//失败则休眠并重试(递归)
            Thread.sleep(50);//单位:ms
            //如果担心递归造成爆栈,可以用循环,一样的
            return queryWithMutex(id);
        }
        //成功就直接开始重建(查询数据库并处理后保存到Redis)
        shop = getById(id);
        //由于在这里我们的数据库在本地,重建很快,所以添加休眠时间来模拟数据库重建过程的耗时长,方便测试
        Thread.sleep(200);
        //不存在,返回错误
        if (shop == null){
            //存一个null到Redis中
            //这种没用的信息,TTL没必要设置太长了,这里我设置成了2min
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //存在,写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }finally {
        //释放互斥锁
        unlock(lockKey);
    }
    //返回
    return shop;
}

queryById方法调用这个方法即可:

    @Override
    public Result queryById(Long id) {
        //缓存穿透的代码调用
        //Shop shop = queryWithPassThrough(id);
        //互斥锁解决缓存击穿
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        //返回
        return Result.ok(shop);
    }

测试

我们先把缓存清空,然后运行代码,再进行测试,我们可以使用一个很好用的测试工具jmeter,下载可以看下面这个博主的文章:
jmeter下载教程
官网下载的实在是太慢了,建议大家直接去这个博主的网盘里面下载。
我们可以这样设定,我们设定我们的并发量为1000,即有1000个线程同时访问:
在这里插入图片描述
并配置好我们的http请求:
在这里插入图片描述
运行完以后打开查看结果树:
在这里插入图片描述
数据都查到了。
打开汇总报告可以看到我们的吞吐量接近200:
在这里插入图片描述
如此大的数据量打下去,但是我们的日志显示我们的数据库只执行了一次的查询语句:
在这里插入图片描述
说明我们已经使用互斥锁成功避免了缓存击穿。

逻辑过期解决缓存击穿

代码实现

根据上次讲解的逻辑过期解决缓存击穿问题的方式,我们可以将客户端查询数据的流程修改为如下:
在这里插入图片描述
其实通常情况,因为是热点key,一般都是会出现在Redis里面的,且因为我们没有设置TTL,所以热点key是一定会一直存在的,但为了严谨起见,还是在判定缓存未命中的时候返回空。
我们主要解析一下主要的流程:

1、我们判断一下缓存是否逻辑过期了,如果没有过期,我们直接返回信息到客户端即可
2、如果缓存逻辑过期了,这个线程就尝试获取互斥锁,如果获取成功,说明它是第一个访问Redis的这个过期key的线程,那么这个线程要做2件事:
(1)返回这个旧数据给客户,虽然数据是旧的,但是这是一种暂时的牺牲
(2)开辟新的线程来进行缓存数据的重建,重建完毕就释放这个互斥锁
3、除了第2种情况说的这个线程,其他线程在知道自己访问的数据过期之后,获取互斥锁都会失败,那么这时候只需要直接返还数据给客户就好了,可能是旧数据,也可能是新数据(第一个线程释放锁或者缓存数据重建成功了)

首先我们要对Shop类增加逻辑过期时间这样一个字段,一种方案是直接添加,这种会违背开闭原则,一种是可以新增加一个类,并且类中包含了逻辑过期时间expireTime,但是该怎么把这个属性添加到Shop里面呢?可以让Shop继承这个类,就可以获得这个类中的属性,但同样会修改Shop这个类的源代码,同样违背开闭原则,所以最好的方法就是用关联来代替继承:
在这里插入图片描述
如果不是很清楚的话可以先去学一下Java设计模式。

为了方便演示,我们首先要进行一个数据预热,我们需要有这个一个带着逻辑过期时间的店铺信息,因此这里就先用单元测试的方式,来让带着expireTime属性的数据存入Redis里面。

1、先编写一个类,用于将带着逻辑过期时间的Shop存入Redis里面:

    public void saveShop2Redis(Long id, Long expireSeconds){
        //查询店铺数据
        Shop shop = getById(id);
        //封装逻辑过期时间
        RedisData redisData = new RedisData();
        //注入Shop对象
        redisData.setData(shop);
        //注入逻辑过期时间
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //写入Redis,不要设置TTL
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

2、编写单元测试代码:

@SpringBootTest
class HmDianPingApplicationTests {

    @Resource
    private ShopServiceImpl shopService;

    @Test
    void testSaveShop(){
        shopService.saveShop2Redis(1L, 10L);
    }

}

我们运行这个单元测试后,打开Redis客户端:
在这里插入图片描述
证明已经存储成功。

现在正式开始用逻辑过期思想去解决缓存击穿的问题,编写这个方法:

	private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    public Shop queryWithLogicalExpire(Long id){
        String key = CACHE_SHOP_KEY + id;
        //从Redis中查询商铺缓存,存储对象可以用String或者Hash,这里用String
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            //未命中,直接返回
            return null;
        }
        //命中,先把json反序列化成对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();//注意我们获取到了data信息以后,返回的会是一个JSONObject的格式
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())){
            //未过期,直接返回店铺信息
            return shop;
        }
        //已过期,缓存重建
        //获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;//LOCK_SHOP_KEY="lock:shop:"
        boolean isLock = tryLock(lockKey);
        //判断是否获取锁成功
        if (isLock){
            //成功获取锁,开启独立线程来实现缓存重建,用线程池来做
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 重建缓存
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }
        //没有成功获取,直接返回过期商铺信息
        return shop;
    }

为了方便验证,之前的缓存重建函数我们给他增加一个模拟重建时间200ms:

    public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
        //查询店铺数据
        Shop shop = getById(id);
        Thread.sleep(200);
        //封装逻辑过期时间
        RedisData redisData = new RedisData();
        //注入Shop对象
        redisData.setData(shop);
        //注入逻辑过期时间
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //写入Redis,不要设置TTL
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

之前存储在Redis的数据,其实早就已经逻辑过期了,只是因为我们没有设置TTL,它依旧存在于Redis里面,只是可能已经过期了。
queryById直接改成调用该方法,接着运行代码。

测试

1、我们直接将数据库中的餐厅名从101修改为102,使得数据库数据与缓存不一致:
在这里插入图片描述

2、打开jmeter进行测试,这里把线程数改小一点,不然不好观察:
在这里插入图片描述
运行后,观察结果树,可以发现我们一开始查到的就是旧数据,且这个过程会持续一段时间:
在这里插入图片描述
到后面,查询到的数据就是正常的数据了:
在这里插入图片描述
查看idea后台,可以发现我们只执行了一次重构,说明只有一个线程操作了数据库,其他数据库都被互斥锁拦住了:
在这里插入图片描述
这也证明了逻辑过期方法会造成短暂的数据不一致的情况。

综上,缓存击穿问题的两种解决思想,其demo都已经调通。

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

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

相关文章

web左侧伸缩菜单栏/导航栏

效果展示&#xff1a; 百度网盘链接下载全部资源&#xff1a; http://链接&#xff1a;https://pan.baidu.com/s/1ZnKdWxTH49JhqZ7Xd-cJIA?pwd4332 提取码&#xff1a;4332 html/JQuery代码&#xff1a; <!DOCTYPE html> <html lang"zh"> <head&g…

2023年全国职业院校技能大赛(高职组)“云计算应用”赛项赛卷②

2023年全国职业院校技能大赛&#xff08;高职组&#xff09; “云计算应用”赛项赛卷2 目录 需要竞赛软件包环境以及备赛资源可私信博主&#xff01;&#xff01;&#xff01; 2023年全国职业院校技能大赛&#xff08;高职组&#xff09; “云计算应用”赛项赛卷2 模块一 …

Spring MVC自定义类型转换器!!!

使用场景 在index.jsp里面添加日期类型 <form action"account/saveAccount" method"post">账户名称&#xff1a;<input type"text" name"name"><br/>账户金额&#xff1a;<input type"text" name&quo…

TCP的这些特性你知道吗?(滑动窗口篇)

如果每次 [发送方] 需要等待接受方返回数据才能发送下一条数据&#xff0c;会导致网络通信的效率非常的低&#xff0c;于是在TCP中 引入了窗口这个概念&#xff0c;即使在往返时间较长的情况下&#xff0c;它也不会降低网络通信的效率。有了窗口&#xff0c;并不代表可以无限 接…

windows rtmp发送数据流程抓包

一、connect 返回Window Acknowledgement Size&#xff1a; 返回Set Peer Bandwidth 二、 releaseStream 三、 FCPublish 四、 createStream 五、 _checkbw 六、 FCPublish返回 七、createStream 八、 _checkbw返回 九、发送关键帧 十、发送视频帧 十一、FCUnpublish 十二、del…

【Verilog】运算符

系列文章 数值&#xff08;整数&#xff0c;实数&#xff0c;字符串&#xff09;与数据类型&#xff08;wire、reg、mem、parameter&#xff09; 系列文章算术运算符关系运算符相等关系运算符逻辑运算符按位运算符归约运算符移位运算符条件运算符连接和复制运算符 算术运算符 …

生成模型 | 2024年新年新论文:audio2photoreal[正在更新中]

本博客主要包含了20240103新出的论文From Audio to Photoreal Embodiment: Synthesizing Humans in Conversations论文解释及项目实现~ 论文题目&#xff1a;20240103_From Audio to Photoreal Embodiment: Synthesizing Humans in Conversations 论文地址&#xff1a;2401.018…

Python2048小游戏核心算法(python系列26)

前言&#xff1a;做核心算法之前我们可以玩一玩这个小游戏来了解一下规则。2048在线试玩 运行效果&#xff1a; 代码案例&#xff1a; # 2048小游戏# 1.将列表中零移动到列表的末尾 def move_zeroes():x 0for i in range(len(list_nums)):if list_nums[i] ! 0:list_nums[x],…

python 基础笔记

基本数据类型 函数 lamda 匿名函数 成员方法 类 类与对象 构造方法 魔术方法 私有成员 私有方法 继承 注解 变量注解 函数注解 Union类型 多态 参考链接&#xff1a;黑马程序员python教程&#xff0c;8天python从入门到精通&#xff0c;学python看这套就够了_哔哩哔哩_bilib…

小测一下HCL中VSR的转发性能

正文共&#xff1a;555 字 10 图&#xff0c;预估阅读时间&#xff1a;1 分钟 上次我们在HCL中导入了NFV的自定义镜像&#xff08;如何在最新版的HCL 5.10.0中导入NFV镜像&#xff1f;&#xff09;&#xff0c;但是当时没有测试转发性能&#xff0c;最近HCL又更新了V5.10.1版本…

用golang 实现给图片添加文字水印

package mainimport ("fmt""github.com/golang/freetype""image""image/draw""image/jpeg""io""os""time" )func main() {// 打开原始图片file, err : os.Open("004.jpeg")if err …

[Vulnhub靶机] DriftingBlues: 5

[Vulnhub靶机] DriftingBlues: 5靶机渗透思路及方法&#xff08;个人分享&#xff09; 靶机下载地址&#xff1a; https://download.vulnhub.com/driftingblues/driftingblues5_vh.ova 靶机地址&#xff1a;192.168.67.24 攻击机地址&#xff1a;192.168.67.3 一、信息收集 …

多线程基础入门【Linux之旅】——下篇【死锁,条件变量,生产消费者模型,信号量】

目录 一&#xff0c;死锁 1. 死锁的必要条件 2&#xff0c;避免死锁 二&#xff0c;条件变量 同步概念与竞态条件 条件变量——初始化 静态初始化 动态初始化 pthread_cond_destroy (销毁) pthread_cond_wait (等待条件满足) pthread_cond_signal (唤醒线程) ph…

@PolarDB,你的动手体验搭子,来啦

前言 PolarDB是阿里云自研的新一代云原生数据库&#xff0c;在计算存储分离架构下&#xff0c;利用了软硬件结合的优势&#xff0c;为用户提供具备极致弹性、高性能、海量存储、安全可靠的数据库服务。100%兼容MySQL和PostgreSQL生态&#xff0c;高度兼容Oracle语法。 1月17日…

Linux下进程控制

文章目录 创建进程fork创建进程fork返回值写诗拷贝fork常规用法fork失败的原因 进程终止进程正常终止查看进程退出码_exit函数exit函数exit 和 _exit 的区别return退出 进程等待进程等待的方式wait方法(系统调用)waitpid方法(系统调用) WEXITSTATUS 和 WIFEXITED阻塞等待和非阻…

vue无法获取dom

处理过程 watch监听值变化 index.js:33 [Vue warn]: Error in callback for watcher "$store.state.modelsStorageUrl": "TypeError: Cannot set properties of undefined (setting modelScene)"watch: {"$store.state.modelsStorageUrl":{ha…

24上半年教师资格证笔试报名全流程✅

&#x1f550;考试时间安排 &#xff08;一&#xff09;报名时间&#xff1a; 2024年1月12日9:30至15日16:00 &#xff08;二&#xff09;缴费时间&#xff1a; 2024年1月17日24:00 &#xff08;三&#xff09;考试时间&#xff1a; 2024年3月9日星期六 &#xff08;四&#xf…

上网行为管理到底是什么!

之前经常听过上网行为管理系统&#xff0c;那这个系统到底是什么&#xff0c;还真不是很清楚&#xff01;今天仔细查了很多资料&#xff0c;分享给大家&#xff1a; 上网行为管理是指一种帮助互联网用户控制和管理对互联网的使用&#xff0c;包括对网页访问过滤、上网隐私保护…

vue3 useAttrs的使用场景,提取共有props

1 场景 多个类似组件都需要传参data&#xff0c;用于请求接口或者处理数据&#xff0c;想让组件干净整洁&#xff0c;把参数data提出来 2 方法 选项式 可以使用mixin混入或者extends继承&#xff08;略&#xff09; 组合式 可以使用hook 无脑式踩坑&#xff08;如下代码…

SwiftUI之深入解析Alignment Guides的超实用实战教程

一、Alignment Guide 简介 Alignment guides 是一个强大的布局工具&#xff0c;但通常未被充分利用。在很多情况下&#xff0c;它们可以帮助我们避免更复杂的选项&#xff0c;比如锚点偏好。如下所示&#xff0c;对对齐的更改也可以自动&#xff08;并且容易地&#xff09;动画…