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://www.rhkb.cn/news/265182.html

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

相关文章

List去重有几种方式

目录 1、for循环添加去重 2、for 双循环去重 3、for 双循环重复坐标去重 4、Set去重 5、stream流去重 1、for循环添加去重 List<String> oldList new ArrayList<>();oldList.add("张三");oldList.add("张三");oldList.add("李四&q…

电子电器架构新趋势 —— 最佳着力点:域控制器

电子电器架构新趋势 —— 最佳着力点&#xff1a;域控制器 我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师&#xff08;Wechat&#xff1a;gongkenan2013&#xff09;。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师…

Qt6.8 GRPC功能使用(2)标准 Qt实现客户端

简介 基于之前的文章所说&#xff0c; Qt6.7之后才开始支持客户端、服务端、及双向流&#xff0c;恰好电脑需要重装&#xff0c;看到Qt6.8版本就直接安装了&#xff0c;内容也是使用Qt6.8的版本进行编译的 客户端实现步骤 1. 安装Qt6.8, 包含GRPC功能模块 Qt 6.8安装目录下包…

SQL注入漏洞解析--less-7

我们先看一下第七关 页面显示use outfile意思是利用文件上传来做 outfile是将检索到的数据&#xff0c;保存到服务器的文件内&#xff1a; 格式&#xff1a;select * into outfile "文件地址" 示例&#xff1a; mysql> select * into outfile f:/mysql/test/one f…

ui设计:利用即使设计设计出漂亮样式

目录 一、基本操作 二、具体介绍 6-1 填充图片 6-2 填充色 6-3 图标 右边栏基础设置 右边栏导出​编辑 一、基本操作 二、具体介绍 6-1 填充图片 选择其一图片填充 6-2 填充色 6-3 图标 右边栏基础设置 右边栏导出

uniapp微信小程序-项目实战修改密码

图标是使用uview里面的图标&#xff0c;icfont也可以 以下是所有代码 <template><view><!-- 密码三个 --><view class"password" v-for"(item,index) in userList"><view class"contentuser"><view class&qu…

LeetCode 热题 100 | 二叉树(二)

目录 1 543. 二叉树的直径 2 102. 二叉树的层序遍历 3 108. 将有序数组转换为二叉搜索树 菜鸟做题&#xff0c;语言是 C 1 543. 二叉树的直径 这道题和 124. 二叉树中的最大路径和 太像了 题眼&#xff1a;二叉树的 直径 是指树中任意两个节点之间 最长路径的长度 。…

软件分层(数据结构/软件逻辑上分层+举例),相连节点的概念+如何相连,为什么是层状结构(软件分层,网络协议分层+梳理协议顺序),协议分层(打电话例子)

目录 软件分层 介绍 举例 类的继承 虚拟文件系统 线程接口封装 虚拟地址空间 总结 为什么是层状的 软件分层 网络协议 原因 梳理协议顺序 相连节点 协议分层 引入 示例 实际上 逻辑上 制定出协议 软件分层 介绍 通过将软件系统划分为不同的层次,每一层都有…

常见的socket函数封装和多进程和多线程实现服务器并发

常见的socket函数封装和多进程和多线程实现服务器并发 1.常见的socket函数封装2.多进程和多线程实现服务器的并发2.1多进程服务器2.2多线程服务器2.3运行效果 1.常见的socket函数封装 accept函数或者read函数是阻塞函数&#xff0c;会被信号打断&#xff0c;我们不能让它停止&a…

人像背景分割SDK,智能图像处理

美摄科技人像背景分割SDK解决方案&#xff1a;引领企业步入智能图像处理新时代 随着科技的不断进步&#xff0c;图像处理技术已成为许多行业不可或缺的一部分。为了满足企业对于高质量、高效率人像背景分割的需求&#xff0c;美摄科技推出了一款领先的人像背景分割SDK&#xf…

自媒体博客Spimes主题源码 X7.0 | Typecho主题模版

Spimes主题专为博客、自媒体、资讯类的网站设计开发&#xff0c;自适应兼容手机、平板设备。一款简约新闻自媒体类的 Typecho 主题&#xff0c;设计上简约、干净、精致、响应式&#xff0c;后台设置更是强大而且实用的新闻自媒体类主题。 PS&#xff1a;5.0版本改动比较多&…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的生活垃圾检测与分类系统(Python+PySide6界面+训练代码)

摘要&#xff1a;本篇博客详细讲述了如何利用深度学习构建一个生活垃圾检测与分类系统&#xff0c;并且提供了完整的实现代码。该系统基于强大的YOLOv8算法&#xff0c;并进行了与前代算法YOLOv7、YOLOv6、YOLOv5的细致对比&#xff0c;展示了其在图像、视频、实时视频流和批量…

前端架构: 脚手架命令行交互核心实现之inquirer和readline的应用教程

命令行交互核心实现 核心目标&#xff1a;实现命令行行交互&#xff0c;如List命令行的交互呢比命令行的渲难度要更大&#xff0c;因为它涉及的技术点会会更多它涉及以下技术点 键盘输入的一个监听 (这里通过 readline来实现)计算命令行窗口的尺寸清屏光标的移动输出流的静默 …

网络初识(概念入门)

目录 1.局域网VS广域网 1.1局域网 1.2广域网 2.五元组 2.1 IP和端口 2.1.1 IP 2.1.2端口号 2.2协议 3.协议分层 4. TCP/IP五层模型 5.封装和分用 5.1封装 5.2分用 1.局域网VS广域网 1.1局域网 简单介绍&#xff1a;指在某一特定区域内由多台计算机组成的互联网组…

vue3个人网站电子宠物

预览 具体代码 Attack.gif Attacked.gif Static.gif Walk.gif Attack.gif Static.gif Attacked.gif Walk.gif <template><div class"pet-container" ref"petContainer"><p class"pet-msg">{{ pet.msg }}</p><img re…

vscode与vue/react环境配置

一、下载并安装VScode 安装VScode 官网下载 二、配置node.js环境 安装node.js 官网下载 会自动配置环境变量和安装npm包(npm的作用就是对Node.js依赖的包进行管理)&#xff0c;此时可以执行 node -v 和 npm -v 分别查看node和npm的版本号&#xff1a; 配置系统变量 因为在执…

【C++进阶】STL容器--list底层剖析(迭代器封装)

目录 前言 list的结构与框架 list迭代器 list的插入和删除 insert erase list析构函数和拷贝构造 析构函数 拷贝构造 赋值重载 迭代器拷贝构造、析构函数实现问题 const迭代器 思考 总结 前言 前边我们了解了list的一些使用及其注意事项&#xff0c;今天我们进一步深入…

LeetCode53题:最大子数组和(python3)

代码思路&#xff1a; 动态规划&#xff0c;使用动态规划如果上一个数是大于0&#xff0c;则加上&#xff1b;如果小于0直接用0。这样做的好处就是最终直接是最大子数组和。 class Solution:def maxSubArray(self, nums: List[int]) -> int:for i in range(1,len(nums)):nu…

ubuntu+QT+ OpenGL环境搭建和绘图

一&#xff0c;安装OpenGL库 安装OpenGL依赖项&#xff1a;运行sudo apt install libgl1-mesa-glx命令安装OpenGL所需的一些依赖项。 安装OpenGL头文件&#xff1a;运行sudo apt install libgl1-mesa-dev命令来安装OpenGL的头文件。 安装GLUT库&#xff1a;GLUT&#xff08;Ope…

express+mysql+vue,从零搭建一个商城管理系统5--用户注册

提示&#xff1a;学习express&#xff0c;搭建管理系统 文章目录 前言一、新建user表二、安装bcryptjs、MD5、body-parser三、修改config/db.js四、新建config/bcrypt.js五、新建models文件夹和models/user.js五、index.js引入使用body-parser六、修改routes/user.js七、启动项…