Redis实战案例8-缓存击穿及其解决方案和案例说明

1. 缓存击穿

缓存击穿是指一个被频繁访问(高并发访问并且缓存重建业务较复杂)的缓存键因为过期失效,同时又有大量并发请求访问此键,导致请求直接落到数据库或后端服务上,增加了系统的负载并可能导致系统崩溃

在这里插入图片描述

常见的解决方案两种:
互斥锁
逻辑过期

在这里插入图片描述

在这里插入图片描述

互斥锁的优点是它可以确保只有一个线程在访问缓存内容,并且在缓存中没有命中时,只会读取一次后端数据库(或其他数据源),其余线程会等待读取完毕后再次读取缓存,这避免了大量的并发请求直接落到后端,从而减少了并发压力,保证系统的稳定性,并且可以保证数据的一致性;
互斥锁的缺点是会增加单个请求的响应时间,因为只有一个线程能够读取缓存值,其他线程则需要等待,这可能会在高并发场景下导致线程池饱和

逻辑过期的优点是可以减少缓存的更新次数,避免在没有必要的情况下过多地读取后端数据源,并且在数据本身有频繁更新的情况下可以避免缓存数据过时;
逻辑过期的缺点是在某些极端情况下会出现缓存为空的情况,如果此时恰巧有大量请求同时访问缓存,则可能导致缓存击穿,并且无法避免大量的并发请求直接落到后端,并且实现起来也是比较复杂和数据无法保证一致性(因为可能返回旧数据)。

2. 互斥锁解决缓存击穿问题

要与缓存穿透区分开来

在这里插入图片描述

使用 setnx 命令(在Java封装Redis功能的API中使用的是setIfAbsent()方法)可以实现互斥锁的功能, setnx 命令可以原子性地设置一个关键字,如果关键字不存在,则设置并返回 1,如果关键字已存在,则不做任何操作并返回 0;
在 Redis 中,该命令会在关键字不存在时将其设置为指定的值(锁),同时返回设置结果,因此可以在线程尝试去设置同一个关键字时,只有一个线程能够成功获取锁,其他线程会返回设置失败;
在释放锁时,应该先对锁进行校验(如,判断当前操作是否为拥有锁的线程),然后再执行删除操作,以确保当前线程不会释放其他线程加的锁;
最后在高并发的场景中,这种操作是会增加Redis服务器的负载的,因此需要合理设置 Redis 参数和优化 Redis 集群架构(在Redis高级中给出方案);

Boolean 类型的值可以自动拆箱为 boolean 类型的值,但是在进行自动拆箱时,如果 Boolean 类型的值为 null,则会抛出 NullPointerException 异常。因此,在进行自动拆箱时,需要注意可能出现的空指针异常。
在使用 Redis 进行分布式锁时,setIfAbsent 方法返回的是 Boolean 类型的值,因此在返回该值时,可能会出现自动拆箱引发的空指针异常。如果出现空指针异常,一般是因为 Redis 连接池未初始化或注入失败,或者 Redis 服务出现了故障。
加锁和释放锁的代码:

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);
}

封装之前缓存穿透的代码:

/*** 缓存穿透功能封装* @param id* @return*/
public Shop queryWithPassThrough(Long id) {String key = CACHE_SHOP_KEY + id;//1. 从Redis中查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//2. 判断是否存在if(StrUtil.isNotBlank(shopJson)){//3. 存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}// 这里要先判断命中的是否是null,因为是null的话也是被上面逻辑判断为不存在// 这里要做缓存穿透处理,所以要对null多做一次判断,如果命中的是null则shopJson为""if("".equals(shopJson)){return null;}//4. 不存在,根据id查询数据库Shop byId = getById(id);if(byId == null) {//5. 不存在,将null写入redis,以便下次继续查询缓存时,如果还是查询空值可以直接返回false信息stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//6. 存在,写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(byId), CACHE_SHOP_TTL, TimeUnit.MINUTES);//7. 返回return byId;
}

合并封装缓存穿透和使用互斥锁实现缓存击穿的代码:

/*** 缓存击穿和缓存穿透功能合并封装* @param id* @return*/
public Shop queryWithMutex(Long id) {String key = CACHE_SHOP_KEY + id;//1. 从Redis中查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//2. 判断是否存在if(StrUtil.isNotBlank(shopJson)){//3. 存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}// 这里要先判断命中的是否是null,因为是null的话也是被上面逻辑判断为不存在// 这里要做缓存穿透处理,所以要对null多做一次判断,如果命中的是null则shopJson为""if("".equals(shopJson)){return null;}//4. 实现缓存重建//4.1 获取互斥锁String lockKey = LOCK_SHOP_KEY + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);//4.2 判断获取是否成功if(!isLock) {//4.3 失败,则休眠并重试Thread.sleep(50);// 递归重试return queryWithMutex(id);}//4.4 成功,根据id查询数据库shop = getById(id);if(shop == null) {//5. 不存在,将null写入redis,以便下次继续查询缓存时,如果还是查询空值可以直接返回false信息stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//6. 存在,写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {//7. 释放互斥锁unLock(lockKey);}//8. 返回return shop;
}

这里并发测试采用JMeter

在这里插入图片描述
在这里插入图片描述

也可以看出查询数据库的操作只出现了一次

在这里插入图片描述

3. 逻辑过期方式解决缓存击穿问题

这里案例中的缓存key默认为高频key,这类key可看做是不会过期,所以这里不做缓存穿透和击穿处理;
这里获取锁之后,如果获取到了则是开启独立线程进行查询操作,如果没有获取到则是直接返回旧数据;
容易出现数据不一致;

在这里插入图片描述

将数据写入Redis,需要设置一个逻辑的过期时间;
目前写入的Shop是没有逻辑过期时间的字段,直接在该类中加入字段不推荐,有侵入性;
新建一个类,设置一个data属性来存Shop对象数据,并且添加字段逻辑过期时间;

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

注:热点key一般是提前写入的,缓存预热;
缓存预热代码示例,要进行单元测试,以便热点key缓存写入Redis中:

/*** 给热点key缓存预热* @param id* @param expireSeconds*/
private void saveShop2Redis(Long id, Long expireSeconds) {// 1.查询店铺数据Shop shop = getById(id);// 2.封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));// 3.写入RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

对我们提前写入的key进行逻辑过期方式处理,解决缓存击穿问题;
这里要避免更新过期数据,当缓存失效(逻辑过期),部分线程经过逻辑过期判断之后,会进行获取锁的操作并进入阻塞状态,可能已经有线程通过更新缓存已经将数据写入缓存中了,这是该线程会释放锁。如果不进行二次逻辑过期判断,当前等待互斥锁的线程可能会将已经更新的数据再次从数据库中读取并写入缓存,导致缓存中存储的是重复的数据。通过逻辑过期判断,可以避免这种重复更新的情况发生。

/*** 缓存击穿(逻辑过期)功能封装* @param id* @return*/
public Shop queryWithLogicalExpire(Long id) {String key = CACHE_SHOP_KEY + id;//1. 从Redis中查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//2. 判断是否存在if(StrUtil.isBlank(shopJson)){//3. 不存在,直接返回(这里做的事热点key预热,所以已经假定热点key已经在缓存中)return null;}//4. 存在,需要判断过期时间,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();//5. 判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {//5.1 未过期,直接返回店铺信息return shop;}//5.2 已过期,需要缓存重建//6. 缓存重建//6.1 获取互斥锁String lockKey = LOCK_SHOP_KEY + id;//6.2 判断是否获取锁成功boolean isLock = tryLock(lockKey);if(isLock) {// 二次验证是否过期,防止多线程下出现缓存重建多次String shopJson2 = stringRedisTemplate.opsForValue().get(key);// 这里假定key存在,所以不做存在校验// 存在,需要判断过期时间,需要先把json反序列化为对象RedisData redisData2 = JSONUtil.toBean(shopJson2, RedisData.class);Shop shop2 = JSONUtil.toBean((JSONObject) redisData2.getData(), Shop.class);LocalDateTime expireTime2 = redisData2.getExpireTime();if(expireTime2.isAfter(LocalDateTime.now())) {// 未过期,直接返回店铺信息return shop2;}//6.3 成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 重建缓存,这里设置的值小一点,方便观察程序执行效果,实际开发应该设为30minthis.saveShop2Redis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放锁unLock(lockKey);}});}//7. 返回return shop;
}

对逻辑过期处理缓存击穿测试;
启动服务,并进行测试(将热点key值写入);

@SpringBootTest
class HmDianPingApplicationTests {@Resourceprivate ShopServiceImpl shopService;@Testvoid testSaveShop() throws InterruptedException {shopService.saveShop2Redis(1L, 10L);}}

在这里插入图片描述

这里修改一下数据数据,好观察重建缓存;

在这里插入图片描述

启动JMeter测试多线程下结果变化;
可以看出在更新缓存前获得的是旧数据,更新之后是新数据;

在这里插入图片描述

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据库性能测试

目录 前言: 1.引入数据库驱动包 2.添加数据库配置元件 3、JDBCRequest参数化 4、Variablesnames参数使用方法: 前言: 数据库性能测试是测试数据库系统在各种条件下的性能和稳定性的过程。它可以帮助测试人员识别数据库系统的性能瓶颈&a…

【Zookeeper】win安装随笔

目录 下载地址下载目标解压后目录结构配置文件配置文件详情伪分布式安装LinuxZooKeeper audit is disabled启动解决报错:SLF4J: Class path contains multiple SLF4J bindings. _ 下载地址 https://zookeeper.apache.org/releases.html 下载目标 记住选择带bin的…

Elasticsearch:使用 Elasticsearch 矢量搜索和 FastAPI 构建文本搜索应用程序

在我的文章 “Elastic:开发者上手指南” 的 “NLP - 自然语言处理及矢量搜索”,我对 Elastic Stack 所提供的矢量搜索有大量的描述。其中很多的方法需要使用到 huggingface.co 及 Elastic 的机器学习。这个对于许多的开发者来说,意味着付费使…

【数据库一】MySQL数据库初体验

MySQL数据库初体验 1.数据库基本概念1.1 数据Data1.2 表1.3 数据库1.4 数据库管理系统1.5 数据库系统 2.数据库的发展3.主流的数据库介绍3.1 SQL Server(微软公司产品)3.2 Oracle (甲骨文公司产品)3.3 DB2(IBM公司产品…

io.netty学习(十三)Netty 解码器

目录 前言 编解码概述 编解码器概述 Netty 内嵌的编码器 解码器 ByteToMessageDecoder 抽象类 ReplayingDecoder 抽象类 MessageToMessageDecoder 抽象类 总结 前言 编码和解码:数据从一种特定协议格式到另一种格式的转换。 处理编码和解码的程序通常被称…

uin-app项目实现pdf文件预览以及下载

由于项目需要,需要对于pdf格式的文件进行预览由用户进行选择性下载,查阅相关文档后方知针对于这种 pdf.js有奇效 一、下载 官网地址https://mozilla.github.io/pdf.js/getting_started/#download 文档下载解压成功后,按照这种格式放入uin-…

计算机基本组成和冯诺依曼机

计算机基本组成和冯诺依曼机 计算机的基本组成 计算机硬件组成 软件与硬件的逻辑等价性 冯诺依曼计算机硬件结构 冯诺依曼计算机工作原理 程序存储控制原理 计算机采用二进制的优势 高电平与低电平电压波动受影响的可能性会降低,抗干扰能力强 什么是冯诺依曼计算机…

DBeaver连接GaussDB

DBeaver 官网:https://dbeaver.io/打开DBeaver,点击菜单栏 “数据库”>“驱动管理” 点击“新建” 填入下面内容: 驱动名称:GS 驱动类型:Generic 类名:org.postgresql.Driver URL模板:jdbc…

在pycharm上导出Anaconda3的环境配置文件

目录 1.原理: ​2.亲身实践: 1.原理: 要在PyCharm中导出Anaconda3环境的配置文件,可以使用conda命令行工具来完成。请按照以下步骤进行操作: 打开PyCharm,并确保项目使用的是Anaconda3环境。 在PyCha…

Linux训练营(文件和目录操作)

文章目录 前言一、ls命令二、cd命令三、mkdir命令四、cp命令五、rm命令总结 前言 本篇文章我们来讲解Linux中的文件和目录操作,在这里我们主要使用的是Linux中的命令来操作这些文件和目录,命令是Linux中最基础的部分。 一、ls命令 ls是一个常用的命令…

css基础知识十一:CSS3新增了哪些新特性?

一、是什么 css,即层叠样式表(Cascading Style Sheets)的简称,是一种标记语言,由浏览器解释执行用来使页面变得更为美观 css3是css的最新标准,是向后兼容的,CSS1/2的特性在CSS3 里都是可以使用…

SpringBoot源码分析(三):SpringBoot的事件分发机制

文章目录 通过源码明晰的几个问题Spring 中的事件Springboot 是怎么做到事件监听的另外两种注册的Listener 源码解析加载listenerSpringApplicationRunListenerEventPublishingRunListenerSimpleApplicationEventMulticaster判断 listener 是否可以接收事件Java 泛型获取 整体流…