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

2024-02-28 11:20

本文主要是介绍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 接口的方式来完成。

这篇关于11 Redis之高并发问题(读+写) + 缓存预热+分布式锁的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/755365

相关文章

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

缓存雪崩问题

缓存雪崩是缓存中大量key失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。 解决方案: 1、使用锁进行控制 2、对同一类型信息的key设置不同的过期时间 3、缓存预热 1. 什么是缓存雪崩 缓存雪崩是指在短时间内,大量缓存数据同时失效,导致所有请求直接涌向数据库,瞬间增加数据库的负载压力,可能导致数据库性能下降甚至崩溃。这种情况往往发生在缓存中大量 k

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

高并发环境中保持幂等性

在高并发环境中保持幂等性是一项重要的挑战。幂等性指的是无论操作执行多少次,其效果都是相同的。确保操作的幂等性可以避免重复执行带来的副作用。以下是一些保持幂等性的常用方法: 唯一标识符: 请求唯一标识:在每次请求中引入唯一标识符(如 UUID 或者生成的唯一 ID),在处理请求时,系统可以检查这个标识符是否已经处理过,如果是,则忽略重复请求。幂等键(Idempotency Key):客户端在每次

【VUE】跨域问题的概念,以及解决方法。

目录 1.跨域概念 2.解决方法 2.1 配置网络请求代理 2.2 使用@CrossOrigin 注解 2.3 通过配置文件实现跨域 2.4 添加 CorsWebFilter 来解决跨域问题 1.跨域概念 跨域问题是由于浏览器实施了同源策略,该策略要求请求的域名、协议和端口必须与提供资源的服务相同。如果不相同,则需要服务器显式地允许这种跨域请求。一般在springbo

题目1254:N皇后问题

题目1254:N皇后问题 时间限制:1 秒 内存限制:128 兆 特殊判题:否 题目描述: N皇后问题,即在N*N的方格棋盘内放置了N个皇后,使得它们不相互攻击(即任意2个皇后不允许处在同一排,同一列,也不允许处在同一斜线上。因为皇后可以直走,横走和斜走如下图)。 你的任务是,对于给定的N,求出有多少种合法的放置方法。输出N皇后问题所有不同的摆放情况个数。 输入

vscode中文乱码问题,注释,终端,调试乱码一劳永逸版

忘记咋回事突然出现了乱码问题,很多方法都试了,注释乱码解决了,终端又乱码,调试窗口也乱码,最后经过本人不懈努力,终于全部解决了,现在分享给大家我的方法。 乱码的原因是各个地方用的编码格式不统一,所以把他们设成统一的utf8. 1.电脑的编码格式 开始-设置-时间和语言-语言和区域 管理语言设置-更改系统区域设置-勾选Bata版:使用utf8-确定-然后按指示重启 2.vscode