11 Redis之高并发问题(读+写) + 缓存预热+分布式锁

8. 高并发问题

Redis做缓存虽减轻了DBMS的压力,减小了RT(Response Time),但在高并发情况下也是可能会出现各种问题的。

8.1 缓存穿透

当用户访问的数据既不在数据库中也不在缓存中,如id为“-1”的数据或id为特别大不存在的数据, 这时的用户很可能是攻击者,攻击会导致数据库压力过大。就会导致每个用户查询都会“穿透”缓存“直抵”数据库。这种情况就称为缓存穿透。
当高度发的访问请求到达时,缓存穿透不仅增加了响应时间,而且还会引发对DBMS的高并发查询,这种高并发查询很可能会导致DBMS的崩溃(对DBMS做的负载均衡暂且不提)。

缓存穿透产生的主要原因有两个:

  • 一是在数据库中没有相应的查询结果,
  • 二是查询结果为空时,不对查询结果进行缓存。

所以,针对以上两点,解决方案也有两个:

  • 对非法请求进行限制,例如限制查询的范围
    a. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
    b. 使用布隆过滤器,需要安装redis组件
    c. 使用布谷鸟滤器,布谷鸟过滤器是布隆过滤器的升级版,需要安装redis组件

  • 对数据库中查询结果也为空的查询给出默认值, 并且把这个键值对的缓存有效时间可以设置短一些,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

8.2 缓存击穿

关键词:定点打击

试想如果所有请求对着一个 key 照死里搞,这是不是就是一种定点打击呢?

怎么理解呢?举个极端的例子:比如某某明星爆出一个惊天狠料,海量吃瓜群众同时访问微博去查看该八卦新闻,而微博 Redis 集群中数据在此刻正好过期了,那么无数的请求则直接打到了微博系统的物理 DB 上,DB 瞬间挂了。

缓存击穿指的就是某key长期有大量请求,但某一瞬间却过期了,那么程序在redis找不到数据,就会去数据库里查询,数据库处理大量的请求的同时导致压力瞬间增大,甚至导致崩溃.
这种情况称为缓存击穿,而该缓存数据称为热点数据。

解决方案:

  • 设置key值永不过期
  • 将key的过期时间设为随机
  • 使用布隆过滤器或者布谷鸟过滤器
  • 使用分布式锁,当多个key过期时,同一时间只有一个查询请求下发到数据库,其他的key等待一个个地轮流查,就可以避免数据库压力过大的问题;代码如下:
   // 分布式锁,为了可读性高用 ReentrantLock 代替分布式锁static Lock lock = new ReentrantLock();public String getData(String key ) throws InterruptedException {try {// 从redis获取值String data =  getRedisData(key);// 如果key不存在,从数据库查询if(null  == data){// 尝试获取锁if(!lock.tryLock()){// 获取锁失败 ,100ms后在次尝试TimeUnit.MILLISECONDS.sleep(100);data = getData(key);}// 走到这里表示成功获取锁// 从myqsl中获取锁data = getMysqlData(key);// 将数据更新到redissetDataToRedis(key,value);}return data;} catch (Exception e){e.printStackTrace();throw e;} finally {// 解锁lock.unlock();}}
  • 双重检测锁机制

8.3.1 穿透和击穿的区别

关于穿透和击穿的区别上面已经介绍的很清楚了,这里在做个总结

  • 穿透 :大量请求了缓存和数据库中都没有的数据,每次都查询数据库,导致数据库压力过大
  • 击穿 : 热点key在同一时间过期,导致所有请求都达到数据库,导致数据库压力过大

8.3 缓存雪崩

关键词:Redis 崩了,没有数据了

这里的 Redis 崩了指的并不是 Redis 集群宕机了。而是说在某个时刻 Redis 集群中的热点 key 都失效了。
如果集群中的热点 key 在某一时刻同时失效了的话,试想海量的请求都将直接打到 DB 上,DB 可能在瞬间就被打爆了,一旦DB崩了,它所带来的连锁反应是可怕的,数据库不可用的情况下你的服务器也无法使用;这就是雪崩效应。

对于缓存雪崩没有很直接的解决方案,最好的解决方案就是预防,即提前规划好缓存的过期时间。要么就是让缓存永久有效,当 DB 中数据发生变化时清除相应的缓存。

如果 DBMS采用的是分布式部署,则将热点数据均匀分布在不同数据库节点中,将可能到来的访问负载均衡开来。

8.4 数据库缓存双写不一致

以上三种情况都是针对高并发读场景中可能会出现的问题,

而在高并发写场景下 , 则可能出现数据库缓存双写不一致的问题

对于数据库缓存双写不一致问题,又分为两种

8.4.1 “修改 DB 并更新缓存”场景

若多个请求要对 DBMS 中同一个数据进行修改,修改后还需要更新缓存中相关数据,
那么程序的异步执行可能会导致缓存与数据库中数据不一致的情况
在这里插入图片描述

8.4.2 “修改 DB 并删除缓存”场景

若两个请求对 DBMS 中同一个数据的操作既包含写也包含读,
且修改后还要删除缓存中相关数据,那么程序的异步执行就可能导致缓存与数据库中数据不一致的情况。

在很多系统中是没有缓存预热 warmup 功能的,为了保持缓存与数据库数据的一致性,一般都是在对数据库执行了写操作后,就会删除相应缓存。

在这里插入图片描述

8.4.3 解决方案

8.4.3.1 延迟双删

延迟双删方案是专门针对于“修改 DB 并删除缓存”场景的解决方案。但该方案并不能彻底解决数据不一致的状况,其只可能降低发生数据不一致的概率。

延迟双删方案是指,在写操作完毕后会立即执行一次缓存的删除操作,然后再停上一段时间(一般为几秒)后再进行一次删除。而两次删除中间的间隔时长,要大于一次缓存写操作
在这里插入图片描述

8.4.3.2 队列

以上两种场景中,只所以会出现数据库与缓存中数据不一致,主要是因为对请求的处理出现了并行。

只要将请求写入到一个统一的队列,只有处理完一个请求后才可处理下一个请求,即让系统对用户请求的处理串行化,就可以完全解决数据不一致的问题。

例如使用ZooKeeper或分布式消息队列MQ

8.4.3.3 分布式锁

使用队列的串行化虽然可以解决数据库与缓存中数据不一致,但系统失去了并发性,降低了性能。

使用分布式锁可以在不影响并发性的前提下,协调各处理线程间的关系,使数据库与缓存中的数据达成一致性。

只需要对数据库中的这个共享数据的访问通过分布式锁来协调对其的操作访问即可


9. 分布式锁

在分布式环境下, 分布式锁大部分是由Lua实现的

9.1 分布式锁的工作原理

当有多个线程要访问某一个共享资源(DBMS 中的数据或 Redis 中的数据,或共享文件等)时,为了达到协调多个线程的同步访问,此时就需要使用分布式锁了。

为了达到同步访问的目的,规定,让这些线程在访问共享资源之前先要获取到一个令牌token,只有具有令牌的线程才可以访问共享资源。这个令牌就是通过各种技术实现的分布式锁。而这个分布锁是一种“互斥资源”,即只有一个。只要有线程抢到了锁,那么其它线程只能等待,直到锁被释放或等待超时。

9.2 问题引入

某电商平台要对商品 sk:0008 进行秒杀销售。假设参与秒杀的商品数量amount 为 1000 台,每个账户只允许抢购一台,即每个请求只会减少一台库存

9.2.1 SB实现

9.2.1.1 准备
  1. 添加spring-boot-starter-redis/web依赖
  2. 编写配置文件设置Redis的主机地址和端口号
    总之,在过去一年中,虽然我在各方面都取得了一些进步,但是离一名优秀共产党员的标准和要求还有一定差距,还存在一些缺点需要克服,主要体现在工作的主动性还不够、服务一线员工的意识还有待加强、思想认识还有待提高等。我相信,在以后的工作学习中,我一定会在党组织的关怀下,在各位党员及同事的帮助日下,通过自己的努力、采取有效措施克服缺点,不断积累经验,提高自身素质、增强工作能力,使自己真正成为一名能经受任何考验的共产党员。
    以上是自己一年来基本情况的小结,不妥之处,恳请党组织批评指正,作为一名预备党员,我渴望按期转为中共正式党员,请党组织考虑我的申请,我将虚心接受党组织对我的审查和考验!如果党组织批准我成为正式党员,我一定在党组织和广大群众的监督之下,牢记入党誓言,勤奋工作、刻苦学习,处处以党员标准严格要求自己,做一名合格的共产党员,如果党组织没有批准我成为正式党员,我也不会泄气,继续努力,争取早日成为一名中国共产党正式党员。
9.2.1.1 有问题的示例

这里仅编写一个controller

@RestController
public class Seckillcontroller {@AutowiredStringRedisTemplate srt;@GetMapping("/sk")public string seckillHandler(){String stock =srt.opsForValue().get("sk:0008");int amount =stock == null ?0 : Integer.parseInt(stock);if(amount>0){srt.opsForValue().set("sk:0008",String.value0f(--amount));return "库存剩余"+ amount +"台";}return"抱歉,您没抢到";
}

上述代码是有问题的。既然是秒杀,那么一定是高并发场景,且生产环境下,该应用一定是部署在一个集群中的。如果参与秒杀的用户数量特别巨大,那么一定会存在很多用户同时读取 Redis 缓存中的 sk:0008 这个 key,那么大家读取到的 value 很可能是相同的,均大于零,均可购买。此时就会出现“超卖”。
即,以上代码存在并发问题。

9.2.1.2 SETNX修改

为了解决上述代码中的并发问题,可以使用 Redis 实现的分布式锁。

该实现方式主要是通过 SETNX 命令完成的。其基本原理是,SETNX 只有在指定 key 不存在时才能执行成功,分布式系统中的哪个节点抢先成功执行了 SETNX ,谁就抢到了锁,谁就拥有了对共享资源的操作权限。
与此同时,其它节点只能等待锁的释放。一旦拥有锁的节点对共享资源操作完毕,其就可以主动删除该 key,即释放锁。然后其它节点就可重新使用 SETNX 命令抢注该 key,即抢注锁

新建一个SeckillController类:

@RestController
public class SeckillController {// 分布式锁的keypublic static final String REDIS_LOCK = "redis_lock";@Autowiredprivate stringRedisTemplate srt;@Value("${server.port}")private String serverPort;@GetMapping(©~"/sk2")public string seckillHandler2(){String result ="抱歉,您没抢到";//仅当try {//setIfAbsent实质就是SETNX, 仅当原键不存在时才能设置Boolean lockOK = srt.opsForValve().setIfAbsent(REDIS_LOCK, "I'm a Lock");if(!lockOK){return "没抢到锁哟";}String stock =srt.opsForValue().get("sk:0008");//如果stock为null, 即缓存中没获取到, 就将amount设为0, 宣告购买失败int amount = stock == null ? 0 : Integer.parseInt(stock);//因为每个人只买一件, 如果库存大于0, 则肯定能买到, 故将amount-1后写回缓存 if (amount >0){srt.opsForValue().set("sk:0008",String.value0f(--amount));result = "库存剩余"+ amount +"台";System.out.println(result);} 
} finally {srt.delete(REDIS_LOCK);}return result +"。server is "+ serverPort;
}

10. 缓存预热warmup

缓存预热指的是提前将热点数据加载到缓存中,这样当用户或系统开始请求这些数据时,它们已经可用,无需等待数据从慢速存储(如数据库)中检索。这有助于避免冷启动问题,提高系统的响应速度和吞吐量。

对于具有缓存 warmup 功能的系统,DBMS 中常用数据的变更,都会引发缓存中相关数据的更新。

Redis缓存预热的场景:

  • 系统重启或部署: 重新部署应用程序后,缓存可能会被清空,预热可以迅速恢复缓存状态。
  • 数据更新: 当缓存中的数据定期更新时,预热可以确保最新数据的快速可用性。
  • 流量高峰: 在预期流量高峰之前预热缓存,可以帮助系统更好地应对负载。

10.1 实现方案

在 Spring Boot 启动之后,可以通过以下四种方案实现缓存预热:

  • 使用启动监听事件实现缓存预热。
  • 使用 @PostConstruct 注解实现缓存预热。
  • 使用 CommandLineRunner 或 ApplicationRunner 实现缓存预热。
  • 通过实现 InitializingBean 接口,并重写 afterPropertiesSet 方法实现缓存预热。

10.1.1 使用启动监听事件实现缓存预热

① 启动监听事件

可以使用 ApplicationListener 监听 ContextRefreshedEvent 或 ApplicationReadyEvent 等应用上下文初始化完成事件,在这些事件触发后执行数据加载到缓存的操作,具体实现如下:

@Component
public class CacheWarmer implements ApplicationListener<ContextRefreshedEvent> {@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {// 执行缓存预热业务...cacheManager.put("key", dataList);}
}

或监听 ApplicationReadyEvent 事件,如下代码所示:

@Component
public class CacheWarmer implements ApplicationListener<ApplicationReadyEvent> {@Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {// 执行缓存预热业务...cacheManager.put("key", dataList);}
}

② @PostConstruct 注解

在需要进行缓存预热的类上添加 @Component 注解,并在其方法中添加 @PostConstruct 注解和缓存预热的业务逻辑,具体实现代码如下:

@Component
public class CachePreloader {@Autowiredprivate YourCacheManager cacheManager;@PostConstructpublic void preloadCache() {// 执行缓存预热业务...cacheManager.put("key", dataList);}
}

③ CommandLineRunner或ApplicationRunner

CommandLineRunner 和 ApplicationRunner 都是 Spring Boot 应用程序启动后要执行的接口,它们都允许我们在应用启动后执行一些自定义的初始化逻辑,例如缓存预热。
CommandLineRunner 实现示例如下:

@Component
public class MyCommandLineRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {// 执行缓存预热业务...cacheManager.put("key", dataList);}
}

ApplicationRunner 实现示例如下:

@Component
public class MyApplicationRunner implements ApplicationRunner {@Overridepublic void run(ApplicationArguments args) throws Exception {// 执行缓存预热业务...cacheManager.put("key", dataList);}
}

CommandLineRunner 和 ApplicationRunner 区别如下:

方法签名不同: CommandLineRunner 接口有一个 run(String... args) 方法,它接收命令行参数作为可变长度字符串数组。ApplicationRunner 接口则提供了一个 run(ApplicationArguments args) 方法,它接收一个 ApplicationArguments 对象作为参数,这个对象提供了对传入的所有命令行参数(包括选项和非选项参数)的访问。
参数解析方式不同: CommandLineRunner 接口更简单直接,适合处理简单的命令行参数。ApplicationRunner 接口提供了一种更强大的参数解析能力,可以通过 ApplicationArguments 获取详细的参数信息,比如获取选项参数及其值、非选项参数列表以及查询是否存在特定参数等。
使用场景不同: 当只需要处理一组简单的命令行参数时,可以使用 CommandLineRunner。对于需要精细控制和解析命令行参数的复杂场景,推荐使用 ApplicationRunner。

④ 实现InitializingBean接口

实现 InitializingBean 接口并重写 afterPropertiesSet 方法,可以在 Spring Bean 初始化完成后执行缓存预热,具体实现代码如下:

@Component
public class CachePreloader implements InitializingBean {@Autowiredprivate YourCacheManager cacheManager;@Overridepublic void afterPropertiesSet() throws Exception {// 执行缓存预热业务...cacheManager.put("key", dataList);}
}

小结

缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。它可以通过监听 ContextRefreshedEvent 或 ApplicationReadyEvent 启动事件,或使用 @PostConstruct 注解,或实现 CommandLineRunner 接口、ApplicationRunner 接口,和 InitializingBean 接口的方式来完成。

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

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

相关文章

数据库相关概念

数据库&#xff08;DataBase&#xff0c;简称DB)&#xff1a;存储数据的仓库&#xff0c;数据是有组织的进行存储。 数据库管理系统&#xff08;DataBass Management System&#xff0c;简称DBMS&#xff09;&#xff1a;操纵和管理数据库的大型软件。 SQL&#xff08;structur…

贪心算法练习day2

删除字符 1.题目及要求 2.解题思路 1&#xff09;初始化最小字母为‘Z’&#xff0c;确保任何字母都能与之比较 2&#xff09;遍历单词&#xff0c;找到当前未删除字母中的最小字母 3&#xff09;获取当前位置的字母 current word.charAt(i)&#xff1b; 4&#xff09;删…

Unity(第十部)时间函数和文件函数

时间函数 using System.Collections; using System.Collections.Generic; using UnityEngine;public class game : MonoBehaviour {// Start is called before the first frame updatefloat timer 0;void Start(){//游戏开始到现在所花的时间Debug.Log(Time.time);//时间缩放值…

C#理论 —— WPF 应用程序Console 控制台应用

文章目录 1. WPF 应用程序1.1 工程创建1.2 控件1.2.1 控件的公共属性1.2.1 TextBox 文本框1.2.1 Button 按钮 *. Console 控制台应用1.1 工程创建 1. WPF 应用程序 1.1 工程创建 Visual Studio 中新建项目 - 选择WPF 应用程序&#xff1b; 1.2 控件 1.2.1 控件的公共属性 …

Linux运维-Web服务器的配置与管理(Apache+tomcat)(没成功,最后有失败经验)

Web服务器的配置与管理(Apachetomcat) 项目场景 公司业务经过长期发展&#xff0c;有了很大突破&#xff0c;已经实现盈利&#xff0c;现公司要求加强技术架构应用功能和安全性以及开始向企业应用、移动APP等领域延伸&#xff0c;此时原来开发web服务的php语言已经不适应新的…

在Web UI上提交Flink作业

1&#xff09;任务打包完成后&#xff0c;我们打开Flink的WEB UI页面&#xff0c;在右侧导航栏点击“Submit New Job”&#xff0c;然后点击按钮“ Add New”&#xff0c;选择要上传运行的JAR包 JAR包上传完成&#xff0c;如下图所示 &#xff08;2&#xff09;点击该JAR包&…

【六袆-Golang】Golang:安装与配置Delve进行Go语言Debug调试

安装与配置Delve进行Go语言Debug调试 一、Delve简介二、win-安装Delve三、使用Delve调试Go程序[命令行的方式]四、使用Golang调试程序 Golang开发工具系列&#xff1a;安装与配置Delve进行Go语言Debug调试 摘要&#xff1a; 开发环境中安装和配置Delve&#xff0c;一个强大的G…

云服务器ECS价格表出炉_2024年最新价格表——阿里云

2024年最新阿里云服务器租用费用优惠价格表&#xff0c;轻量2核2G3M带宽轻量服务器一年61元&#xff0c;折合5元1个月&#xff0c;新老用户同享99元一年服务器&#xff0c;2核4G5M服务器ECS优惠价199元一年&#xff0c;2核4G4M轻量服务器165元一年&#xff0c;2核4G服务器30元3…

from tensorflow.keras.layers import Dense,Flatten,Input报错无法引用

from tensorflow.keras.layers import Dense,Flatten,Input 打印一下路径&#xff1a; import tensorflow as tf import keras print(tf.__path__) print(keras.__path__) [E:\\开发工具\\pythonProject\\studyLL\\venv\\lib\\site-packages\\keras\\api\\_v2, E:\\开发工具\\…

虚拟机中window7界面太小解决办法

1.在虚拟机中的桌面的空白处右击&#xff0c;然后点击屏幕分辨率 2.根据自己电脑屏幕的大小来选择对应分辨率

EMR StarRocks实战——Mysql数据实时同步到SR

文章摘抄阿里云EMR上的StarRocks实践&#xff1a;《基于实时计算Flink使用CTAS&CDAS功能同步MySQL数据至StarRocks》 前言 CTAS可以实现单表的结构和数据同步&#xff0c;CDAS可以实现整库同步或者同一库中的多表结构和数据同步。下文主要介绍如何使用Flink平台和E-MapRed…

Biotin-PEG2-Thiol,生物素-PEG2-巯基,应用于抗体标记、蛋白质富集等领域

您好&#xff0c;欢迎来到新研之家 文章关键词&#xff1a;Biotin-PEG2-Thiol&#xff0c;生物素-PEG2-巯基&#xff0c;Biotin PEG2 Thiol&#xff0c;生物素 PEG2 巯基 一、基本信息 【产品简介】&#xff1a;Biotin PEG2 Thiol can bind with antibodies to prepare biot…

【C语言】学生宿舍信息管理系统

目录 项目说明 1. 数据结构设计 2. 功能实现 3. 主菜单设计 4. 文件操作 5. 系统使用 项目展示 1.主菜单功能界面 ​编辑 2.添加信息 3.查询信息 4.修改信息 5.删除信息 6.退出程序 项目完整代码 结语 在这篇博客中&#xff0c;我们将探讨如何使用C语言来开发…

React Switch用法及手写Switch实现

问&#xff1a;如果注册的路由特别多&#xff0c;找到一个匹配项以后还会一直往下找&#xff0c;我们想让react找到一个匹配项以后不再继续了&#xff0c;怎么处理&#xff1f;答&#xff1a;<Switch>独特之处在于它只绘制子元素中第一个匹配的路由元素。 如果没有<Sw…

腾讯云服务器4核8G性能,和阿里云比怎么样?

腾讯云4核8G服务器支持多少人在线访问&#xff1f;支持25人同时访问。实际上程序效率不同支持人数在线人数不同&#xff0c;公网带宽也是影响4核8G服务器并发数的一大因素&#xff0c;假设公网带宽太小&#xff0c;流量直接卡在入口&#xff0c;4核8G配置的CPU内存也会造成计算…

Adobe illustrator CEP插件调试

1.创建插件CEP面板&#xff0c;可以参考&#xff1a;http://blog.nullice.com/%E6%8A%80%E6%9C%AF/CEP-%E5%BC%80%E5%8F%91%E6%95%99%E7%A8%8B/%E6%8A%80%E6%9C%AF-CEP-%E5%BC%80%E5%8F%91%E6%95%99%E7%A8%8B-Adobe-CEP-%E6%89%A9%E5%B1%95%E5%BC%80%E5%8F%91%E6%95%99%E7%A8%8…

C++ 补充之常用遍历算法

C遍历算法和原理 C标准库提供了丰富的遍历算法&#xff0c;涵盖了各种不同的功能。以下是一些常见的C遍历算法以及它们的概念和原理的简要讲解&#xff1a; for_each&#xff1a;对容器中的每个元素应用指定的函数。 概念&#xff1a;对于给定的容器和一个可调用对象&#xff…

4_怎么看原理图之协议类接口之SPI笔记

SPI&#xff08;Serial Peripheral Interface&#xff09;是一种同步串行通信协议&#xff0c;通常用于在芯片之间传输数据。SPI协议使用四根线进行通信&#xff1a;主设备发送数据&#xff08;MOSI&#xff09;&#xff0c;从设备发送数据&#xff08;MISO&#xff09;&#x…

Rust调用同级目录中的rs文件和调用下级目录中的rs文件

一、Rust调用同级目录中的rs文件 Rust新建工程demo02&#xff0c;src文件夹下面新建test.rs文件&#xff0c;这样main.rs文件与它属于同级目录中。 关键点&#xff1a;导入test文件和test文件中的Ellipse模块 mod test;//导入test模块&#xff08;文件&#xff09; use test…

LeetCode 刷题 [C++] 第141题.环形链表

题目描述 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&a…