Redis学习[6] ——Redis缓存设计

news2024/9/22 13:39:29

八、Redis缓存设计

8.1 为什么Redis用作缓存?

一般来说,数据库的数据都是落在磁盘上的,会导致读写速度很慢。如果用户的请求量非常大,数据库很容易崩溃。由于Redis的数据保存在内存中,读写速度很快,所以Redis一般被用作数据库的缓存层。因为 Redis 是内存数据库,我们可以将数据库的数据缓存在 Redis 里,相当于数据缓存在内存,内存的读写速度比硬盘快好几个数量级,这样大大提高了系统性能

<img src="G:\code\study\CppStudy\docs\figures\redis缓存.

png" alt=“图片” style=“zoom:66%;” />

引入了缓存层,就会有缓存异常的三个问题,分别是**缓存雪崩、缓存击穿、缓存穿透**。

在这里插入图片描述

在这里插入图片描述

8.2 什么是缓存雪崩?

8.2.1 什么是缓存雪崩?

通常来说,为了保证缓存和数据库中的数据一致性,需要给Redis缓存中的数据**设置过期时间**。当缓存过期后,则用户的请求会从数据库读取数据,并将数据再次存入到Redis缓存中。

当**大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增**,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

发生缓存雪崩有两个原因:

  • 大量数据同时过期
  • Redis 故障宕机
8.2.2 大量数据同时过期解决方案?

针对大量数据同时过期而引发的缓存雪崩问题,常见的应对方法有下面这几种:

  • 均匀设置过期时间,避免大量数据同时过期

    我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。

  • 互斥锁

    如果一个请求发现访问的数据不在Redis中,则先**加互斥锁,保证同一时刻只有一个请求从数据库中读取数据,并更新到Redis缓存中,完成请求后再释放锁。没有拿到锁的请求则会阻塞等待后重新读取缓存(而不需要再去访问数据库)**,或者直接返回空值或默认值。

    实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。

  • 后台更新缓存

    不再给缓存设置过期时间,而是让缓存**“永久有效”,开启一个后台线程定时更新缓存。事实上,当系统内存资源紧张时,对一些缓存数据进行“淘汰”。这就需要机制来保障数据的有效性,Redis一般通过后台线程监控(就是定时更新缓存这个线程)或者业务线程等方式来通知数据库读取数据更新这个淘汰的缓存**。

8.2.3 Redis故障宕机解决方案?

针对 Redis 故障宕机而引发的缓存雪崩问题,常见的应对方法有下面这几种:

  • 服务熔断或请求限流机制

    • 启动**服务熔断**机制:暂停业务应用对缓存服务的访问,直接返回错误,不用再继续访问数据库,从而降低对数据库的访问压力,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。
    • 启动**请求限流**机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。
  • 构建Redis缓存高可靠集群

    通过主从节点的方式构建 Redis 缓存高可靠集群。如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。

8.3 什么是缓存击穿?

我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为**热点数据。如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿**的问题。

可以发现缓存击穿跟缓存雪崩很相似,可以认为**缓存击穿是缓存雪崩的一个子集**。

应对缓存击穿可以采取前面说到两种方案(除了均匀分配过期时间,因为这个是只针对热点数据,而不是大量数据的):

  • 互斥锁方案:保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
  • 后台更新方案:不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

8.4 什么是缓存穿透?

8.4.1 什么是缓存穿透?

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是**缓存穿透**的问题。

缓存穿透的发生一般有这两种情况:

  • 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
  • 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
8.4.2 如何应对缓存穿透?

应对缓存穿透的方案,常见的方案有三种:

  • 限制非法请求

    在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

  • 缓存空值或者默认值

    当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

  • 布隆过滤器

    在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以**通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库**,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

    布隆过滤器是如何工作的呢?

    布隆过滤器由**「初始值都为 0 的位图数组」「 N 个哈希函数」**两部分组成。布隆过滤器会通过 3 个操作完成标记:

    • 第一步:使用N个哈希函数分别对数据进行运算,得到N个哈希值;
    • 第二步:将N个哈希值对BitMap的长度取模,得到每个哈希值在BitMap数组的位置;
    • 第三步:将N个哈希值所在的BitMap的对应位置的值设为1;

    在这里插入图片描述

    当应用要查询数据 x 是否在数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中。布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时**存在哈希冲突的可能性**,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。

    所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据

8.6 如何设计可以动态缓存热点数据的缓存策略?

由于数据存储受限,系统并不是将所有数据都需要存放到缓存中的,而**只是将其中一部分热点数据缓存起来**,所以我们要设计一个热点数据动态缓存的策略。

热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据

以电商平台场景中的例子,现在要求只缓存用户经常访问的 Top 1000 的商品。具体细节如下:

  • 先通过缓存系统做一个排序队列(比如存放 1000 个商品),系统会根据商品的访问时间,更新队列信息,越是最近访问的商品排名越靠前;
  • 同时系统会定期过滤掉队列中排名最后的 200 个商品,然后再从数据库中随机读取出 200 个商品加入队列中;
  • 这样当请求每次到达的时候,会先从队列中获取商品 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的商品信息,并返回。

在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作。

8.7 常见的缓存策略?

常见的缓存更新策略有3种:

  • Cache Aside(旁路缓存)策略;
  • Read/Write Through (读穿/写穿)策略;
  • Write Back (写回)策略;

实际开发中,Redis 和 MySQL 的更新策略用的是 Cache Aside,另外两种策略应用不了。

8.7.1 Cache Aside(旁路缓存)策略

Cache Aside(旁路缓存)策略是最常用的,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为**「读策略」「写策略」**。

在这里插入图片描述

写策略的步骤

  • 更新数据库中的数据,删除缓存中的数据。

读策略的步骤

  • 如果读取的数据命中了缓存,则直接返回数据;
  • 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。

写策略能否反过来? 先删除缓存数据,再更新数据库数据?

不能,原因是在「读+写」并发的时候,会出现缓存和数据库的数据不一致性的问题。详情见后面分析。

Cache Aside 策略适合读多写少的场景,不适合写多的场景,因为当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。如果业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:

  • 更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了。当然这么做对于写入的性能会有一些影响;
  • 更新数据时更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快过期,对业务的影响也是可以接受。
8.7.2 Read/Write Through(读穿 / 写穿)策略

Read/Write Through(读穿 / 写穿)策略原则是应用程序**只和缓存交互,不再和数据库交互,而是由缓存和数据库交互,相当于更新数据库的操作由缓存自己代理了**。

读策略的步骤

  • 先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。

写策略的步骤

当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在:

  • 如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成;
  • 如果缓存中数据不存在,直接更新数据库,然后返回。

Read Through/Write Through 策略的特点是**由缓存节点而非应用程序来和数据库打交道**,在我们开发过程中相比 Cache Aside 策略要少见一些,原因是我们经常使用的分布式缓存组件,无论是 Memcached 还是 Redis 都不提供写入数据库和自动加载数据库中的数据的功能。而我们在使用本地缓存的时候可以考虑使用这种策略。

8.7.3 Write Back (写回)策略

Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通**过批量异步更新的方式进行**。

Write Back 策略特别适合写多的场景,因为发生写操作的时候, 只需要更新缓存,就立马返回了。比如,写文件的时候,实际上是写入到文件系统的缓存就返回了,并不会写磁盘。

但是带来的问题是,数据不是强一致性的,而且会有数据丢失的风险,因为缓存一般使用内存,而内存是非持久化的,所以一旦缓存机器掉电,就会造成原本缓存中的脏数据丢失。所以你会发现系统在掉电之后,之前写入的文件会有部分丢失,就是因为 Page Cache 还没有来得及刷盘造成的。

8.8 如何保证缓存和数据库数据的一致性?

8.8.1 缓存更新还是删除?先数据库还是先缓存?

无论是**「先更新数据库,再更新缓存」,还是「先更新缓存,再更新数据库」,这两个方案都存在并发问题**,当两个请求并发更新同一条数据的时候,可能会出现缓存和数据库中的数据不一致的现象。于是,Cache Aside策略中使用的是**「先更新数据库,再删除缓存」策略,不对缓存更新,而是直接把缓存给删除,等下次读取数据时从数据库读并加入到缓存中**。

「先删除缓存,再更新数据库」,在「读 + 写」并发的时候,还是会出现缓存和数据库的数据不一致的问题:

在这里插入图片描述

因此,Cache Aside策略中使用的是**「先更新数据库,再删除缓存」策略。理论上分析,先更新数据库,再删除缓存也是会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高**。如下图:

在这里插入图片描述

因为缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。所以,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的

8.8.2 「先更新数据库 + 再删除缓存」如何确保两个操作都能执行成功?

这次用户的投诉是因为在删除缓存(第二个操作)的时候失败了,导致缓存还是旧值,而数据库是最新值,造成数据库和缓存数据不一致的问题,会对敏感业务造成影响。

在这里插入图片描述

该怎么解决呢?有两种方法:

  • 消息队列重试机制
  • 订阅 MySQL binlog,再操作缓存

资料参考

内容大多参考自:图解Redis介绍 | 小林coding (xiaolincoding.com)

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

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

相关文章

SQL注入 报错注入+附加拓展知识,一篇文章带你轻松入门

第5关--------------------------------------------> 前端直接不会显示账号密码的打印&#xff1b;但是在接收前端的数据的那部分后端那里&#xff0c;会看前端传递过来的值是否正确&#xff0c;如果不正确&#xff0c;后端接收值那里就会当MySQL语句执行错误&#xff0c;…

RK3568笔记五十一:W25Q64测试(spi 标准接口 )

若该文为原创文章&#xff0c;转载请注明原文出处。 前面有测试过W25Q64&#xff0c;但那是自己编写的驱动&#xff0c;现在使用内核自带的驱动&#xff0c;只需要通过SPI标准接口&#xff0c;编写应用程序即可以读写W25Q64. 一、硬件原理图 SPI 引脚 功能 MOSI GPIO3_C1 …

【java基础】徒手写Hello, World!程序

文章目录 前提&#xff1a;java环境变量配置使用vscode编写helloworld解析 前提&#xff1a;java环境变量配置 https://blog.csdn.net/xzzteach/article/details/140869188 使用vscode编写helloworld code .为什么用code看下图 报错了&#xff01;&#xff01;&#xff01;&…

【MATLAB】Matlab安装包及验证生成器

通过百度网盘分享的文件&#xff1a;Matlab 链接: https://pan.baidu.com/s/1PF8iP31WFJUYRF7PLyiX2A?pwdxkds 提取码&#xff1a;xkds

简单搭建dns服务器

目录 一.安装服务 二.编写子配置文件 三.编写主配置文件 四.编写文件 五.测试 一.安装服务 [rootnode1 ~]# dnf install bind -y 二.编写子配置文件 [rootnode1 ~]# vim /etc/named.rfc1912.zones 三.编写主配置文件 [rootnode1 ~]# vim /etc/named.conf 四.编写文件 …

一款创新的物联网综合业务支撑平台,提供资费、客户、进销存、合同、订单、续费、充值、账单等功能(附源码)

前言 在当今快速发展的物联网时代&#xff0c;企业和开发者面临着很大的挑战和机遇。现有软件在处理物联网设备和数据管理方面常常存在一些痛点&#xff0c;如设备管理分散、数据同步不及时、用户交互体验不佳等。这些问题不仅影响了物联网解决方案的效率&#xff0c;也限制了…

docker部署可执行的jar

1.将项目打包&#xff0c;上传到服务器的指定目录 2.在该目录下创建Dockerfile文件 3.Dockerfile写入如下指令 # 基于哪个镜像 FROM java:8 # 拷贝文件到容器&#xff0c;也可以直接写成ADD xxxxx.jar /app.jar ADD springboot-file-0.0.1.jar file.jar RUN bash -c touch /…

GuLi商城-商品服务-API-新增商品-调试会员等级相关接口

在网关服务中配置路由: 代码: nacos这些服务都要启动: 如果有不是一个命名空间中的,要改成同一个命名空间中 启动商品product服务遇到循环依赖问题,解决:

AVL树在插入时保持平衡的旋转过程

目录 AVL树节点的定义 AVL树的插入 AVL树的旋转 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查找元素相当于在顺序表中搜索元素&#xff0c;效率低下。于是在这两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.…

《LeetCode热题100》---<6.①矩阵四道(二维数组)>

本篇博客讲解LeetCode热题100道矩阵篇中的四道题 第一道&#xff1a;矩阵置零&#xff08;中等&#xff09; 第二道&#xff1a;螺旋矩阵&#xff08;中等&#xff09; 第一道&#xff1a;矩阵置零&#xff08;中等&#xff09; 方法一&#xff1a;使用标记数组 class Solutio…

C语言指针(1)

目录 一、内存和地址 1、生活中的例子 2、内存的关系 二、指针变量和地址 1、&符号&#xff0c;%p占位符 2、一个简单的指针代码。 3、理解指针 4、解引用操作符 5、指针变量的大小。 三、指针变量类型的意义 1、指针解引用的作用 2、指针指针 3、指针-指针 4…

Leetcode3224. 使差值相等的最少数组改动次数

Every day a Leetcode 题目来源&#xff1a;3224. 使差值相等的最少数组改动次数 解法1&#xff1a; 想一想&#xff0c;什么情况下答案是 0&#xff1f;什么情况下答案是 1&#xff1f; 如果答案是 0&#xff0c;意味着所有 ∣nums[i]−nums[n−1−i]∣ 都等于同一个数 X。…

【JVM内存】系统性排查JVM内存问题的思路

【JVM内存】系统性排查JVM内存问题的思路 背景 前言 遇到过几次JVM堆外内存泄露的问题&#xff0c;每次问题的排查、修复都耗费了不少时间&#xff0c;问题持续几月、甚至一两年。我们将这些排查的思路梳理成一套系统的方法&#xff0c;希望能给对JVM内存分布、内存泄露问题…

有序矩阵中第K小的元素(LeetCode)

题目 给你一个 n x n 矩阵 matrix &#xff0c;其中每行和每列元素均按升序排序&#xff0c;找到矩阵中第 k 小的元素。 请注意&#xff0c;它是 排序后 的第 k 小元素&#xff0c;而不是第 k 个 不同 的元素。 你必须找到一个内存复杂度优于 的解决方案。 解题 from queue i…

DFS之搜索顺序与剪枝

搜索顺序&#xff1a; 1.https://www.acwing.com/problem/content/1119/ 首先&#xff0c;我们考虑一个贪心&#xff1a; 假如说A的倒数K个字符恰好与B的前K个字符重合&#xff0c;那么我们就连接。 也就是说我们一旦匹配就直接相连而不是继续找更长的重合的一段子串。 因…

秋招突击——算法练习——8/3——马上消费笔试总结——{距离为一的字符串、组合数遍历}

文章目录 引言正文第一题&#xff1a;距离为1的字符串个人实现修正实现 第二题&#xff1a;三角形数个人实现反思实现比较对象使用equalsCollections.sort方法 总结 引言 今天的笔试难度不算大&#xff0c;但是自己的做的很糟糕&#xff0c;发现了很多问题&#xff0c;很多模板…

目标检测,目标跟踪,目标追踪

个人专做目标检测&#xff0c;目标跟踪&#xff0c;目标追踪&#xff0c;deepsort。YOLOv5 yolov8 yolov7 yolov3运行指导、环境配置、数据集配置等&#xff08;也可解决代码bug&#xff09;&#xff0c;cpu&#xff0c;gpu&#xff0c;可直接运行&#xff0c;本地安装或者远程…

JVM-类加载器和双亲委派机制

什么是类加载器&#xff1f; 类加载器是Jvm的重要组成之一&#xff08;类加载器、运行时数据区、执行引擎、本地库接口、本地方法库&#xff09;&#xff0c;负责读取java字节码并将其加载到Jvm中的组件 类加载器的分类 Java中的类加载器可以分为以下几种&#xff1a; 1. 启…

Yolov8在RK3588上进行自定义目标检测(一)

1.数据集和训练模型 项目地址&#xff1a;https://github.com/airockchip/ultralytics_yolov8.git 从github(htps:l/github.com/airockchip/ultralytics_yolov8)上获取yolov8模型。 下载项目&#xff1a; git clone https://github.com/airockchip/ultralytics_yolov8.git …

进程的虚拟内存地址(C++程序的内存分区)

严谨的说法&#xff1a; 一个C、C程序实际就是一个进程&#xff0c;那么C的内存分区&#xff0c;实际上就是一个进程的内存分区&#xff0c;这样的话就可以分为两个大模块&#xff0c;从上往下&#xff0c;也就是0地址一直往下&#xff0c;假如是x86的32位Linux系统&#xff0c…