缓存原理的学习

news2024/11/15 17:47:44

在如今这个微服务分布式的大环境下,集群分布式部署 几乎 已经是我们每个人都熟知的了。
缓存也一样,对我们来说 ,如果只是一个单体应用 , 那只要 有本地缓存就足以了,但是倘若分布式部署了很多台机器上,那我们该如何缓存呢,如果依然用本地缓存,那我们不同机器之间的缓存数据该如何同步呢 。这样就需要我们的分布式缓存出场了。下面我将分别介绍 本地缓存 和 分布式缓存。

一、本地缓存

我们通常可以在类中声明一个静态的Map类型变量,以键值对的方式存放我们的数据,需要数据时,直接从map中拿即可。这就是本地缓存。在单体应用下本地缓存毫无疑问是最简单最方便的。但是随着业务的不断扩大,我们的服务仅在一台机器上部署远远不够支撑流量了,于是我们就需要分布式部署,利用负载均衡的手段让请求分散到不同的机器上。这样就会出现下面的两个问题

1、倘若我们第一次请求到第一台机器,本地缓存的map中没有数据,回进行一次查库操作。将数据存入map中。但是第二次请求到了第二台机器,第二台机器的map中依然没值。这样就需要再次查库。

2、倘若是一个修改请求,第一次请求到第一台机器。修改了数据库,同时将本地缓存数据修改了,但是此时一个查询请求到了第二台机器,由于修改的请求是在第一台机器触发的,因此也只有第一台机器的缓存是和数据库一致的,此时读第二台请求,相当于读的还是旧数据。这样就造成了数据一致性的问题。

基于以上的分析,我们发现本地缓存已经无法满足我们分布式下的系统缓存需要了。那么我们就需要引出分布式缓存了。常见的分布式缓存就是我们最熟悉的redis了。分布式缓存的原理无非就是 将缓存的数据统一存到一个地方,让所有机器去这一个地方读取或修改数据即可。同时redis的集群也可以从某种意义上帮助我们实现一台机器内存不够的情况。

二、redis分布式缓存

redis缓存虽然解决了分布式情况下本地缓存带来的一些问题,但是也同时会出现一些新的问题,下面我们就来分析一下这些问题吧。

谈到redis缓存,我们大家都很熟悉的高并发下可能会导致redis缓存失效的三类问题,下面就让我们一起来分析一下吧~

缓存穿透

导致缓存穿透的情况就是 , 当我们访问一个缓存中不存在的数据,就会去查库,这样就会导致缓存未命中疯狂扫射数据库的情况。这样的话我们的缓存也就失去了意义。说的再通俗一点就是,当我们访问一个永远不存在的数据,比方我们查询数据库中商品id为-1的数据,这样的数据在数据库始终就没有存储,缓存中也没有此数据,在这样的情况下,查询缓存未命中,我们就会去查询数据库,但是当查数据库时 , 查到的数据为null,但是并没有将null值写入redis缓存,因此我们下次查询此数据,依然缓存未命中,这样就会导致查询始终都是访问数据库,系统就会出现被 利用不存在的数据进行攻击的风险。导致服务挂掉。

解决方案就是查数据库查询到null,我们也写入redis缓存中,这样即便查不存在的数据,缓存也依然命中。

缓存雪崩

缓存雪崩的意思就像时雪山崩塌,一瞬间大量的redis缓存key过期,并且还是在高并发流量的情况下过期,这样就会大量并发查询请求落到数据库上,导致数据库压力过大直接奔溃。

** 解决方案通常我们可以在设置缓存过期时间时加一个随机值,这样就可以大大降低缓存时间都相同的概率,通常就很难引发缓存集体失效的情况了。**

缓存击穿

缓存击穿的情况就像是 射击一样,盯住一个一直设计,突然这个被放开了,设计就穿过去了。
描述的是,当我们的一个热点key在某一时刻失效,刚好此时大并发同时在访问此key,这样的情况下就会让大量并发在key失效的一瞬间同时去访问数据库。导致数据库奔溃。

对于此问题我们通常的解决方式是加锁,我们在访问数据库的时候上锁。只让一个请求可以到达数据库。其他请求都等待,当到达数据库的请求获取到数据后就可以写入缓存了,写入缓存后,释放锁,其他请求此时排队获取到锁后进入方法,在查数据库之前再去先判断一下缓存是否有数据,发现缓存有数据,直接返回,也就不会落到数据库上了。

再解决缓存击穿时我们提到了加锁,我们可能第一时间想到的就是 synchronized 关键字。或者 Lock。但是Juc(lock)或者synchronized 他们都是本地锁,也就是说只能锁本地服务,当有多台服务器部署服务时,负载均很后的情况通常来说就是 , 每台服务器都会允许一个请求去查库。但是我们是否可以锁所有呢?让多台服务器也只有一个 请求可以去查库。那就提到了我们的分布式锁

下面就让我们来谈谈分布式锁的实现方式吧。

分布式锁

首先我们先来自己实现一下分布式锁。我们想一下本地锁是怎么实现的,本地锁最核心的就是那个同步监视器,我们要求 同步监视器(也就是锁)必须是同一个,这样的话,我们就可以将资源锁住。现在分布式的情况下,问题就是 多台服务器之间无法通信,相当于每台服务器都有一个自己的锁,我们想实现分布式锁,那我们就可以将这个锁同分布式缓存一样,统一放在一个地方,让所有的服务都去这一个地方去获取锁。这样就相当于所有的服务器拿到的都是同一把锁。那么如何实现呢?

在redis中有这样一个命令

set key value NX

这里的这条命令NX的意思就是 如果缓存中有这个key , 那就set失败,如果缓存中没有key就set该key到缓存。而这里,能够set成功的那个就相当于是抢到锁的。

下面我们来看看我们的一步步实现吧。
在这里插入图片描述
在这里插入图片描述

上图这样基本上我们可以说是实现了分布式锁,但是我们需要分析一下上面的实现是否有一些潜在问题。

在上面的这种实现中我们时候存在死锁的可能,假设我们执行完了业务,但是还没来得及删除锁服务宕机了,或者是在执行业务的过程中出了异常。就会导致我们的锁永远都无法被删除了。为了防止这样产生的死锁,我们就需要给锁加一个时效,就是加一个过期时间,到了指定的时间 , 不管怎样都将锁删除,这样就可以解决死锁的情况了。

改进成了这样。
在这里插入图片描述在这里插入图片描述

我们虽然加了过期时间,但是这样的情况下还是有可能导致死锁,为什么呢,我们来分析一下。
假设我们执行到获取锁,服务宕机了,那我们的过期时间相当于还是没设置上,依旧会造成死锁,因此我们必须保证设置锁和设置过期时间是一个原子操作。在redis中我们有这样一个命令 set key value NX EX。我们再来看看实现。
在这里插入图片描述
在这里插入图片描述
改进为这样的代码 , 就保证了设置锁和设置过期时间的原子性。

但是这样的代码是否还存在问题呢?我们的锁是有了过期时间了。但是我们删除锁的时候,我们可以直接删除吗?我们试想一种场景:倘若我们业务执行了50s , 但是我们设置的锁过期时间是30s,在我们业务执行到30s的时候 , 锁就已经被删除了,那么此时其他的线程就可以获取这个锁了,当其他线程拿到这个锁之后,业务执行到50s,结束了,此时这个线程就要执行删除锁的操作了,但是这个线程删除锁直接删除了就相当于把别人的锁给删除掉了。这显然是不对的。那我们该怎么解决这件事呢?我们就需要加一个限制,让每个线程只能删除他自己的锁。怎么限制呢?我们的redis存储都是存的key-vaule键值对呀,我们现在相当于都只是用到了key去保证是一把锁。但是value我们也可以用起来。怎么用呢?
我们在获取锁的时候 , 指定一个value。有那个随机UUID指定即可。每个人删除锁的时候先判断一下,是自己的锁,再删除。
那么代码就如下了。
在这里插入图片描述
在这里插入图片描述
以上操作可以保证每个人只删除自己的锁吗?
答案是无法保证的,为什么还是无法保证呢?让我们一起来分析一下。
在这里插入图片描述

针对以上的情况我们该怎么办呢?
我们就需要保证获取锁的值和删除锁这两个操作要是原子操作,那怎么办呢?参照redis官方文档,我们可以看到我们可以使用Lua脚本保证原子性。
在这里插入图片描述
在这里插入图片描述
这样我们就大工告成了,但是这里还存在一个问题,就是 假设我们业务执行时间特别长,在我们业务执行还没有将数据写入缓存,锁已经过期了,那么此时我们的其他线程就可以获取到锁,但是获取到锁后,发现缓存中依然没有,因此就还会查数据库,这样就相当于我们没锁住。

针对上面最后的这种情况,我们只需要把锁的过期时间设置的长一些,只要保证大于所有业务执行时间即可。但是有没有更优雅的方案呢?当然有,那就是锁的自动续期。这里我们就提到了一个分布式锁的中间件,帮助我们做了这些事情,只需要简单的一些配置即可做到我们以上的实现。那就是redisson。让我们一起来看看吧。

redisson分布式锁

redisson中的锁,我们查看源码发现,他实现了Juc包中的Lock,因此我们在并发编程JUC中所学的在redisson中同样可以使用。没有太大的学习成本,下面就让我们一起来学习一下吧。
在使用redisson锁时 , lock中传的参数即锁的名字。一定要注意,锁的名字一样就是一把锁。 锁的粒度,越细越快。

在redisson中我们只需要 调用 lock.lock();即可加锁,这个方法试阻塞式的锁。默认锁时间式30s。

  • 并且redisson中为我们实现了 锁的自动续期,也就是说 当业务时间超长 , 锁会自动续期,不会中途失效,导致锁不住的情况。
  • 同时redisson中的锁在业务执行完之后会自动删除,也就是在业务执行完之后就不会再续期了,因此,即使未调用 unlock() 锁最终也会被删除,不会产生死锁的情况。
  • 如果我们传递了锁的超时时间,即我们调用的是 lock(10 , TimeUnit.SECONDS)这个方法。就执行脚本,发送给redis进行占锁,默认的超时时间就是我们指定的时间。
  • 如果我们未指定锁的超时时间 , 就使用 30 * 1000;即为看门狗的默认时间lockWatchdogTimeout
  • 未指定超时时间,只要占锁成功,就会启动一个定时任务,重新给锁设置过期时间,新的过期时间就是看门狗的默认时间。1/3 的看门狗时间回自动续期一次。续期为我们的看门狗时间。
    .
    我们使用redisson改造一下我们上面的代码
    在这里插入图片描述
    只需要调用这一个方法就可以做到我们上面写的那一堆代码的功能 , 并且还可以实现锁的自动续期,我们一起来探究一下吧。
  1. 在源码中我们能看到在这里传的参数 是 -1 , 也就是说我们调用lock什么参数都不传的话默认是传一个-1。这个参数是锁的过期时间。我们继续往下跟踪代码。 在这里插入图片描述
  2. 我们来看这个方法尝试获取锁
    在这里插入图片描述
  3. 进入我们会发现在这里会判断这个过期时间的参数
    如果是不等于-1 , 也就是说是我们调用lock时候指定了过期时间 就执行4这个方法 , 值是一个lua脚本。在这里插入图片描述4. 我们进去看发现是一个lua脚本。
    在这里插入图片描述5. 若是我们没指定过期时间的话,我们重点来看这个方法 scheduleExpirationRenewal(threadId);
  1. 进入之后我们重点来看这个方法renewExpiration();
  2. 进入方法后我们发现这里有一个定时任务。去做锁的自动续期。
  3. 具体规则我们可以看到是时间的三分之一每次续期。
    在这里插入图片描述
    这就是我们大概的redisson中简单的加锁的源码。我们可以从中发现 我们调用lock时指定了过期时间的话,redisson就不会为我们进行锁的自动续期了。

下面我们可以再看看redisson中的其他锁。redisson中也为我们提供了诸如 读写锁 、 闭锁 、 信号量 、 等这些锁。
我们一起来探究一下~

读写锁:

写锁:

RReadWriteLock lock = redisson.getReadWriteLock("read-write-lock");
RLock rLock = lock.writeLock();
rLock.lock();

读锁:

RReadWriteLock lock = redisson.getReadWriteLock("read-write-lock");
RLock rLock = lock.readLock();
rLock.lock();
  • 读写锁的需求就是 保证业务一定能读到最新的数据。修改期间,写锁是一个排它锁,也就是互斥锁。读锁是一个共享锁。
  • 写锁没释放,读就必须等待。 并发读只会在redis中记录好,所有当前的读锁,并且他们都会同时加锁成功。

读 + 读 : 相当于无锁。
写 + 读 : 等待写锁释放才能读。
写 + 写 。 阻塞方式。
读 + 写 。 有读锁,写需要等待。
只要有写的存在,都必须等待。

闭锁

就是必须都完成了才能解锁。

  • 获取闭锁: RCountDownLatch door = redisson.getCountDownLatch(“door”);
  • 设置锁的计数次数:door.trySetCount(5);
  • 阻塞等待锁完成:door.await();
  • 锁计数减一 : door.countDown();
/**
     * 放假锁门
     * 必须每个班都没人了才锁门。
     * @return
     */
    @ResponseBody
    @GetMapping("/lockDoor")
    public String lockDoor() throws InterruptedException {

        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.trySetCount(5);
        door.await(); // 等待闭锁都完成。

        return "放假了。。。";
    }

    @ResponseBody
    @GetMapping("/gogogo/{id}")
    public String gogogo(@PathVariable("id") Long id) {

        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.countDown(); // 计数减1。

        return id + "班的人都走了";
    }

信号量

车库停车;3车位。

  • 获取信号量:park.acquire();
  • 释放一个信号量: park.release();
  • 以上两个都是阻塞的,也就是说 ,获取不到的话,会一直阻塞等待。
  • 我们也可以使用 park.tryAcquire() 该方法返回true 或者 false 。 表示 获取成功或失败。也就不会阻塞了。

    /**
     * 车库停车 三车位。
     * @return
     */
    @ResponseBody
    @GetMapping("/park")
    public String park() throws InterruptedException {

        RSemaphore park = redisson.getSemaphore("park");
        park.acquire(); // 获取一个值。获取一个信号。  阻塞 , 获取失败了  会一直等,直到获取成功。  占一个车位。

        return "ok";
    }
    @ResponseBody
    @GetMapping("/go")
    public String go() {

        RSemaphore park = redisson.getSemaphore("park");
        park.release(); // 释放一个车位。

        return "ok";
    }

到这里我们就大概了解了redisson实现的分布式锁相关内容了。

下面我们还需要解决一个缓存应用中可能存在的问题,即缓存一致性的问题。
解决缓存一致性问题我们通常有两种解决方式。

  • 双写模式
  • 失效模式

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

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

相关文章

软考-操作系统

【考点梳理】 【进程管理】 考点1、进程的状态(★★) 【考法分析】 本考点主要考查形式主要是根据图示判断相关状态位置或状态变迁条件。 【要点分析】 操作系统三态模型如下图所示:操作系统五态模型:【备考点拨】 掌握操作…

vue2.0和vue3.0创建项目

由于vue项目依赖于nodejs,所以需要先安装它。没有nodejs去官网下载。 npm install --global vue-cli 国内npm网站很慢,可以使用淘宝镜像 npm install --registryhttps://registry.npm.taobao.org vue2.0创建项目: 进入到自己需要创建项目…

SAP给微信推送消息

导语:最近领导下发指令,要求研究SAP与微信,企业微信,钉钉,邮件推送消息的平台,类似于采购订单审批之后,可以通过以上软件给用户发消息,我认领了微信的部分。 整个研究过程是很痛苦的…

华为静态NAT、动态NAT、PAT端口复用

一、网络环境及TOP 1.1 R1 相当于内网的一台PC, IP:192.168.1.10 网关为 192.168.1.254 [R1]ip route-static 0.0.0.0 0 192.168.1.254 # R1配置默认路由(网关) 1.2 R2为出口路由器,分别连接内网R1及外网R3 1&…

7. 整数反转

题目链接:力扣 解题思路: 题目要求中有一句话:假设环境不允许存储 64 位整数(有符号或无符号)。 也就是说不能使用long类型来判断是否int溢出,只能使用int类型进行运算 首先对于一个整数的翻转比较简单…

学openCV,不会数字图像系统可不行

来源:投稿 作者:小灰灰 编辑:学姐 前言 在光照的情况下,通过成像系统将大自然中的物体拍摄出来,成像系统手机自带有,这里面我们关心的是分辨率,成像系统显示的点数越多,阵列越大&am…

HCIA静态试验(12.30-31复习)

目标实现: 2、首先进行子网划分 基于192.168.1.0 24划分 ‘一共7个路由器需要7个网段还有7个主干网 192.168.1.0/24 ----用于骨干 192.168.1.32/27 ----R1环回 192.168.1.32/28 192.168.1.48/28 192.168.1.64/27 --- R2环回 192.168.1.64/28 192.168.1.80/28 …

Java之网络相关概念

寒假又开始更新java了,之后更新的是b站教程韩顺平老师的课,编译器我从idea换成eclipse(因为蓝桥杯只有eclipse,要先熟悉) 1.网络相关概念 网络通信 网络 ip地址 1.简单来说ip地址是每一台主机的标识 类似于我们现…

Vue 疑难扎症(一)有时候取不到Vue对象中值怎么办?对象值发生改变了但是页面没有刷新怎么办?

目录 有时候取不到对象中值怎么办? 问题截图 问题代码 问题分析 情况1 情况2 情况3 问题解决 对象值发生改变了但是页面没有刷新怎么办? 为什么? 常见错误写法: 怎么办? 有时候取不到对象中值怎么办&…

Node.js--》如何在Node.js中使用中间件,看这一篇就足够了

目录 中间件 中间件函数使用 中间件的作用 中间件分类 使用中间件的注意事项 编写接口 跨域问题及其解决方案 中间件 中间件特指业务流程的中间处理环节。当一个请求到达 Express 的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理…

【网络排查】用于接口不通,mysql,kafka等数据库介质连不上的排查

这篇文章记录生产实践中遇到的网络不通的例子 文章目录前言1. 网络协议1.1 应用层找到有问题的服务端 IP总结前言 接口调用不同了了怎么办? 就找接口服务提供方,肯定是提供方的问题的,跟调用方有啥关系~ kafka,mysql等数据库介质…

JAVAGUI编程初识之Swing

文章目录一 常用窗口1.1 JFrame框架窗口1.2 演示-JFRame,JLable的使用1.3 JDialog标签1.3.1 演示-JDialog标签二 标签组件2.1 标签2.2 图标2.2.1 ICon接口简介2.2.2 演示-用Icon接口创建图标2.3 图片图标2.3.1 演示-图片图标三 布局管理器3.1 绝对布局3.1.1 绝对布局简介3.1.2 …

年末再看指针。看来搞C/C++,如影随形的指针就得门清~~~

继上篇博文因内核页表引出的指针问题,后来又研究了一番,这次应该比较清楚了,这里再总结一下。 目录 0 前言 1 普通指针: 2 指针的指针: 3 普通指针参数: 4 指针的指针参数: 5 函数指针&a…

[Kettle] 认识Kettle

1.初识Kettle Kettle是ETL数据整合与处理工具,翻译成中文是"水壶"的意思,可理解为希望把各种数据放到一个壶里,像水一样以一种指定的格式流出,表达数据流的含义 ETL(Extract - Transform - Load)是将数据从数据来源端…

centos7部署rancher2.5

一、 什么是 Rancher Rancher 是为使用容器的公司打造的容器管理平台。Rancher 简化了使用 Kubernetes 的流程,开发者可以随处运行 Kubernetes(Run Kubernetes Everywhere),满足 IT 需求规范,赋能 DevOps 团队。 Ran…

单纯形法与对偶单纯形法的通俗理解

cigma<0,a>0 min cigma/(a) 决定出基变量 1对偶单纯形法 意思是看c就是所有货物的价值&#xff0c;去看一眼这些货物单价组合售卖的价值&#xff0c;这些价值肯定要都大于0&#xff0c;而且&#xff0c;组成这个c的系数也应该是都是正的&#xff0c; c最小证明对min&a…

港科夜闻|香港科大-越秀集团百万奖金国际创业大赛2022年度前8强20强项目评审结果公布...

关注并星标每周阅读港科夜闻建立新视野 开启新思维1、“香港科大-越秀集团”百万奖金国际创业大赛2022年度前8强&20强项目评审结果公布。2022年赛事中的各赛区前三名项目&#xff0c;共计23个项目自动入围年度总决赛&#xff0c;本轮评审在这23个项目中&#xff0c;评选出了…

Hudi学习02 -- Hudi核心概念

文章目录基本概念时间轴&#xff08;Timeline&#xff09;文件布局&#xff08;File Layout&#xff09;索引&#xff08;Index&#xff09;索引原理索引类型索引的选择策略表类型&#xff08;Table Types&#xff09;查询类型&#xff08;Query Types&#xff09;写操作&#…

Qt第五十二章:Qt Design Studio使用技巧。

一、运行项目和Debugging项目【快捷键&#xff1a;CtrR】 二、 预览单Qml文件 三、添加资源文件 &#xff08;使用资源&#xff1a;将资源拖动到Editor中的矩形中即可&#xff09; 四、多状态【正常状态、按下状态、划过状态、已点击状态...】 注意&#xff1a;多状态看起来像…

java短网址平台

git地址 Reduce: 短网址平台&#xff0c;Coody Framework首秀&#xff0c;自写IOC、MVC、ORM、TASK、JSON、DB连接池、服务器。百毫秒启动&#xff0c;全项目仅2.1M&#xff08;低配服可运行&#xff09; reduce短网址平台 测试站地址&#xff1a;http://dev.icoody.cn/ 技…