谷粒商城----通过缓存和分布式锁获取数据。

embedded/2024/11/14 2:30:39/

高并发下缓存失效的问题

高并发下缓存失效的问题--缓存穿透

指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义

导致缓存穿透的就是红色的这条线

解决方案

null结果缓存,并且设置一个短暂的过期时间。

高并发下缓存失效的问题--缓存雪崩

缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。(大面积Key同时失效)

解决方案

原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低就很难引发集体失效的事件。

高并发下缓存失效的问题--缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

解决方案

加锁 大量并发只让一个去查,其他人等待,查到以后释放锁其他人获取到锁,先查缓存,就会有数据,不用去db

谷粒商城中的锁

接口层

/*** @description: 从服务器中获取一段JSON数据* @param: []* @return: java.util.Map<java.lang.String,java.util.List<com.atguigu.gulimail.product.vo.Catelog2Vo>>*/
@ResponseBody
@GetMapping("/index/catalog.json")
public Map<String, List<Catelog2Vo>> getCatelogJson() {Map<String, List<Catelog2Vo>> catelogJson = categoryService.getCatelogJson();return catelogJson;
}

业务层

getCatelogJson

首先去缓存中获取JSON

String catalogJSONFromCache = redisTemplate.opsForValue().get("catalogJSON");

  • 有 ,通过阿里的fast-json,把redis中的字符串解析成对象,由于该对象是个复杂类型的,所以这里使用TypeReference并用泛型,指定上类型。

  • 没有,调用getCatelogJsonFromDataWithRedisLock,使用缓存锁的情况下,从数据库中获取数据。

@Override
public Map<String, List<Catelog2Vo>> (){
​/*a* 1. 空结果缓存* 2. 设置随机值的过期时间* 3. 缓存击穿* */
​//1.加入缓存的功能String catalogJSONFromCache = redisTemplate.opsForValue().get("catalogJSON");
​if(StringUtils.isEmpty(catalogJSONFromCache)){//2.缓存中没有,查询数据库return getCatelogJsonFromDataWithRedisLock();}//转为指定的对象,使用阿里的fastJSON,如果是复杂的数据类型,需要指定一个TypeReference,这里直接使用匿名内部类,进行转换return JSON.parseObject(catalogJSONFromCache,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
}

getCatelogJsonFromDataWithRedisLock

进入getCatelogJsonFromDataWithRedisLock方法,在这里使用的分布式缓存锁,主要应对缓存失效的问题:

1. 缓存雪崩
1. 缓存穿透
1. 缓存击穿
String token = UUID.randomUUID().toString();Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",token,300,TimeUnit.SECONDS);

这里的锁,必须是要有过期时间的

万一拿到锁的服务,去操作数据库,出现异常,或者机器突然崩掉,就会导致锁无法释放,就会出现死锁的 状况。

set lock "haha" nx

nx:只有在没有这个k-v的情况下,才可以设置成功,否则就是失败的。

问题:

  1. 万一拿到锁的服务,去操作数据库,出现异常,或者机器突然崩掉,就会导致锁无法释放,就会出现死锁的状况。

    1. 解决方法,设置个自动过期时间(一定要设置成原子操作,ex)

    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111",300,TimeUnit.SECONDS);

这里每个人的锁,也就是key,对应的value都是一个不相同的UUID。

如果业务超时,会导致多人都拿到锁,而且前面的线程会删掉后面线程的锁,导致业务异常。

解决方法,设置的Value都不尽相同,可以是UUID随机值,之后在比对值后,才进行删除(一定要设置成 原子操作)。

锁的抢占

redisTemplate.opsForValue().setIfAbsent("lock",token,300,TimeUnit.SECONDS);

这些事朝Redis中放入一个key,如果不存在这个key,设置上,并返回true,如果有这个key,则直接返回false。

可以通过这个结果来判断抢占锁的成功与否。

  • 如果锁抢占失败:

    • 先让线程睡200ms。

    • 然后再次调用这个方法:getCatelogJsonFromDataWithRedisLock,抢占资源。

  • 如果锁抢占成功:

    • 执行正在从数据库或得数据的方法:data = getDataFromDB();

    • 原子操作释放锁。

      • 为什么这里要使用脚本操作?保证原子性。为什么要原子性?

        如果不是原子操作:

 

public Map<String, List<Catelog2Vo>> getCatelogJsonFromDataWithRedisLock() {
​//抢占锁 ,并且设置过期时间(30s之后删除)//每一个线程的锁都是不一样的。String token = UUID.randomUUID().toString();Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",token,300,TimeUnit.SECONDS);//返回的结果if(lock){System.err.println("获取分布式锁成功");Map<String, List<Catelog2Vo>> data = null;try{//加锁成功,执行业务data = getDataFromDB();}finally {String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";Integer result = redisTemplate.execute(new DefaultRedisScript<Integer>(script, Integer.class),Collections.singletonList("lock"),token);}//业务执行成功,解锁
//            redisTemplate.delete("lock");
​//删锁的就需要进行值比对
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if(lockValue.equals(token)){
//                //删除自己的锁
//                redisTemplate.delete("lock");
//            }//返回结果return data;}else{try {Thread.sleep(200);} catch (InterruptedException e) {
​}System.err.println("获取分布式锁不成功...等待重试");//加锁失败,重试return getCatelogJsonFromDataWithRedisLock();}}

getDataFromDB

String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");

这里为什么要进行第二次的查看缓存数据?

getCatelogJson方法中,就进行了一次缓存的读取,为什么这里还需要一次呢?可以想象一个场景:当缓存中没有数据时,用大量并发线程都访问这个方法获取JSON数据,第一次的查缓存肯定是null,所以所有的线程都进入了getCatelogJsonFromDataWithRedisLock方法,排队获得锁,第一个获得锁的线程,读取数据,把内容放到缓存之后,就是剩下的线程获得锁,进入getDataFromDB这个方法,如果不再次查缓存的话,剩下的 线程就还是从数据库中获取数据,因为第一层缓存查询,没有过滤掉这些线程(用词可能不合适),这下就变成排队查数据库了,还是出现了缓存失效的问题。

private Map<String, List<Catelog2Vo>> getDataFromDB() {//拿到锁之后,第一步就应该是去缓存中看一下,是否已经有了内容,如果有了,就不用去操作数据库了。//如果不进行一次,这个锁就失去意义了,就变成排队查数据库了String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");if(!StringUtils.isEmpty(catalogJSON)){//缓存不为空,就返回数据return JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {});}System.err.println("====================================Info-Message================================");List<CategoryEntity> selectList = baseMapper.selectList(null);//1.查出所有1级分类List<CategoryEntity> level1 = getParent_cid(selectList, 0L);//2.封装数据Map<String, List<Catelog2Vo>> parent_cid = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {//1.查出1级分类中所有2级分类List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());//2.封装上面的结果List<Catelog2Vo> catelog2Vos = null;if (categoryEntities != null) {catelog2Vos = categoryEntities.stream().map(l2 -> {Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//查询当前2级分类的3级分类List<CategoryEntity> level3 = getParent_cid(selectList, l2.getCatId());if (level3 != null) {List<Catelog2Vo.Catelog3Vo> collect = level3.stream().map(l3 -> {//封装指定格式Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());return catelog3Vo;}).collect(Collectors.toList());catelog2Vo.setCatalog3List(collect);}return catelog2Vo;}).collect(Collectors.toList());}return catelog2Vos;}));

整个流程


http://www.ppmy.cn/embedded/58238.html

相关文章

go获取正在运行的函数并及时捕获panic

Go 语言中&#xff0c;panic 是一种运行时错误&#xff0c;它会导致当前 goroutine 立即停止执行&#xff0c;并开始逐层向上返回&#xff0c;直到被 recover 捕获或者程序崩溃。panic 通常用于异常情况&#xff0c;比如程序遇到了无法恢复的错误。 捕获 panic 的主要作用包括…

Unity3D 资源管理YooAsset原理分析与详解

引言 Unity3D 是一款广泛应用于游戏开发、虚拟现实&#xff08;VR&#xff09;、增强现实&#xff08;AR&#xff09;等领域的强大游戏开发引擎。在开发过程中&#xff0c;资源管理是一项至关重要的任务&#xff0c;它直接影响到游戏的性能和用户体验。YooAsset 是一个基于 Un…

软件运行次数

题目&#xff1a; 实现一个验证程序运行次数的小程序&#xff0c;要求如下&#xff1a; 当程序运行超过3次时给出提示&#xff1a;本软件只能免费使用3次&#xff0c;欢迎您注册会员后继续使用&#xff5e;程序运行演示如下&#xff1a; 第一次运行控制台输出&#xff1a;欢迎…

VirtualBox 安装 Ubuntu Server24.04

环境&#xff1a; ubuntu-2404-server、virtualbox 7.0.18 新建虚拟机 分配 CPU 核心和内存&#xff08;根据自己电脑实际硬件配置选择&#xff09; 分配磁盘空间&#xff08;根据自己硬盘实际情况和需求分配即可&#xff09; 设置网卡&#xff0c;网卡1 负责上网&#xff0c…

找不到msvcr120.dll无法继续执行代码的原因分析及解决方法

我们可以使用一种科学的方法解决msvcr120.dll丢失的问题。这是由于日常使用电脑时的不当操作&#xff0c;可能会导致一些dll文件的丢失。对于这种情况&#xff0c;我们可以谨慎地修复来解决。 一、首先让我们了解msvcr120.dll是什么及重要性 msvcr120.dll 是微软公司开发的Vis…

Apache Seata应用侧启动过程剖析——RM TM如何与TC建立连接

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 Apache Seata应用侧启动过程剖析——RM & TM如何与TC建立连接 前言 看过官网 README 的第…

【云原生】Kubernetes之持久化

Kubernetes 持久化存储 文章目录 Kubernetes 持久化存储一、为什么要做持久化存储二、都有哪些存储2.1、emptyDir2.1.1、什么是emptyDir2.1.2、emptyDir作用2.1.3、emptyDir的应用场景2.1.4、emptyDir优缺点2.1.5、emptyDir的使用方式 2.2、hostPath2.2.1、什么是hostPath2.2.2…

基于DPU的云原生计算资源共池管理解决方案

1. 方案背景和挑战 在传统的云环境中&#xff0c;通常存在着不同的技术栈&#xff0c;支撑多样化的计算服务&#xff0c;具体如下&#xff1a; ① OpenStack环境与虚拟化云主机及裸金属服务 OpenStack是一个开源的云计算管理平台项目&#xff0c;它提供了部署和管理大规模计…