面经:Redis

文章目录

一、Redis基础

1. 什么是Redis

Redisopen in new window 是一个基于 C 语言开发的开源数据库(BSD 许可),与传统数据库不同的是 Redis 的数据是存在内存中的(内存数据库),读写速度非常快,被广泛应用于缓存方向。并且,Redis 存储的是 KV 键值对数据

  • 内置了多种数据类型实现
  • 支持事务 、持久化、Lua 脚本、多种开箱即用的集群方案
  • 没有外部依赖

2. Redis为什么这么快

  1. Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
  2. Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
  3. Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果
  4. 高效的数据结构:Redis 提供了多种高效的数据结构,如字符串、哈希表、列表、集合和有序集合等。这些数据结构在内存中直接存储和操作数据,使得Redis能够以常数时间复杂度 (O(1))来执行许多常见的操作,如插入、删除和查找;
  5. 异步操作:Redis 支持异步操作,可以将一些耗时的操作(如持久化)放到后台进行,不会阻塞其他的操作;
  6. 轻量级:Redis 本身是一个非常轻量级的软件,它使用 C 语言编写,代码简洁高效。它没有复杂的依赖和额外的抽象层,因此可以更快地启动和运行。

3. 分布式缓存常见的技术选型方案

  • Memcached 和 Redis
  • 腾讯也开源了一款类似于 Redis 的分布式高性能 KV 存储数据库,基于知名的开源项目 RocksDBopen in new window 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型,名为 Tendis (用的少)

4. 对比Redis和Memcached

  • 相同
    (1) 都是基于内存的数据库,一般都用来当做缓存使用
    (2) 都有过期策略
    (3) 两者的性能都非常高
  • 区别
    (1) Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型
    (2) Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中
    (3) Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上
    (4) Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
    (5) Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的。
    (6) Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。
    (7) Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
    (8) Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除

5. Redis为什么用缓存

  • 高性能
    假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。这样有什么好处呢? 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了**。操作缓存就是直接操作内存,所以速度相当快**。
  • 高并发
    一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 Redis 的情况,Redis 集群的话会更高)。
    由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发

6. Redis除了做缓存,还能做什么

  • 分布式锁
  • 限流
  • 消息队列
  • 复杂业务场景

7. Redis可以做消息队列吗

Redis 5.0 新增加的一个数据结构 Stream 可以用来做消息队列,Stream 支持:

  • 发布 / 订阅模式
  • 按照消费者组进行消费
  • 消息持久化( RDB 和 AOF)
    和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此,我们通常建议是不使用 Redis 来做消息队列的,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka。

8. redis与mysql区别

(1)类型上
从类型上来说,mysql是关系型数据库,redis是缓存数据库
(2)作用上
mysql用于持久化的存储数据到硬盘,功能强大,速度较慢,基于磁盘,读写速度没有Redis快,但是不受空间容量限制,性价比高
redis用于存储使用较为频繁的数据到缓存中,读取速度快,基于内存,读写速度快,也可做持久化,但是内存空间有限,当数据量超过内存空间时,需扩充内存,但内存价格贵
(3)需求上
mysql和redis因为需求的不同,一般都是配合使用。
需要高性能的地方使用Redis,不需要高性能的地方使用MySQL。存储数据在MySQL和Redis之间做同步。

9. 三种常用的缓存读写策略

  • Cache Aside Pattern(旁路缓存模式)

  • 在这里插入图片描述
    在这里插入图片描述

  • Read/Write Through Pattern(读写穿透)
    Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。(很少用这种策略)
    在这里插入图片描述
    在这里插入图片描述
    在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。

  • Write Behind Pattern(异步缓存写入)
    Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。

二、Redis数据结构

1. 常用数据结构

添加链接描述

  • Strng(字符串)
    String 类型的底层实现 SDSopen in new window(Simple Dynamic String,简单动态字符串
    应用场景:常规数据(比如 session、token、、序列化后的对象)的缓存;计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数;计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数;
  • List(列表)
    Redis 的 List 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
  • Set((集合)
  • Hash(散列)
  • Zset(有序集合)
  • HyperLogLogs(基数统计)
    (1)应用场景:数量量巨大(百万、千万级别以上)的计数场景,热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计
  • Bitmap(位存储)
    (1) Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
    (2) 应用场景:需要保存状态信息(0/1 即可表示)的场景
    举例 :用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)
  • Geospatial(地理位置)

底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Hash Table(哈希表)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。
在这里插入图片描述

5种基本数据结构

2. String还是Hash存储对象数据更好?

  • 在绝大部分情况,我们建议使用 String 来存储对象数据即可!
  • String 存储的是序列化后的对象数据,存放的是整个对象Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息Hash 就非常适合。
  • String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。

3. 购物车信息: String还是Hash?

以业务比较简单的购物车场景举例
在这里插入图片描述
用户添加商品就是往 Hash 里面增加新的 field 与 value;查询购物车信息就是遍历对应的 Hash;更改商品数量直接修改对应的 value 值(直接 set 或者做运算皆可);删除商品就是删除 Hash 中对应的 field;清空购物车直接删除对应的 key 即可。

4. 实现排行榜

sorted set 的数据结构经常被用在各种排行榜的场景

5. 抽奖系统

使用 Set 实现
SPOP key count : 随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。
SRANDMEMBER key count : 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。

6. 统计活跃用户

Bitmap 使用日期(精确到天)作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1。

SETBIT 20210308 2 1

7. 统计页面 UV

HyperLogLog 是用来做基数统计的算法, 将访问指定页面的每个用户 ID 添加到 HyperLogLog 中。

8. SortedSet数据结构

哈希和跳跃表,
hash就是关联元素value和权重score
跳跃表就是给value排序

  • 跳跃表数据结构
    跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。
    在这里插入图片描述

9.BitMap底层数据结构及应用

底层数据结构就是0/1bit 的二进制数值
在这里插入图片描述

  • 应用
  • 给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中。
    首先,遍历这40亿个数字,分别在对应的位图中进行置位。然后对于给出的数,进行按秩查找,判断是否在bitmap中即可。
  • 在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数
    参考的一个方法是:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)。其实,这里可以使用两个普通的Bitmap,即第一个Bitmap存储的是整数是否出现,如果再次出现,则在第二个Bitmap中设置即可。这样的话,就可以使用简单的1-Bitmap了

10. Ziplist数据结构

ZipList就是当【zset】和【hash】容器对象在元素个数较少或元素长度较短时采用的数据结构。它是一块连续的内存空间,每一个元素都前后挨着,中间没有内存空隙。同时它也是一个经过特殊编码的双向链表,它的设计目标就是为了提高内存存储效率,
ZipList可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作

11. Redis中map扩容(渐进式rehash)

  • 底层有两个dict,一个dict负责请求,到达负载比例进行扩容,渐进式扩容,一部分一部分转移到新的dict
  • rehash和渐进式rehash区别
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

12. 跳表结构

第一层是双向链表,会有多层来作为链表的索引。
二叉查找树的时间复杂度是O(logn),空间复杂度是O(n);跳表的时间复杂度是O(log_{k}n),k为跳表索引步长,空间复杂度是O(n)

三、线程模型

1. Redis单线程模型

  • Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,这套事件处理模型对应的是 Redis 中的文件事件处理器
  • 由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
  • 虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。
    I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗
    理解:这里“多路”「指的是多个网络连接客户端,」“复用”**指的是复用同一个线程(单进程)。I/O 多路复用其实是使用一个线程来检查多个 Socket 的就绪状态,在单个线程中通过记录跟踪每一个 socket(I/O流)的状态来管理处理多个 I/O 流
    在这里插入图片描述
    IO多路复用 参考文献1
    IO多路复用 参考文献2
  • IO多路复用对比NIO
    NIO需要在用户程序的循环语句中不停地检查各个socket是否有数据读入,而IO多路复用在用户程序层面则不需要循环语句,虽然IO多路复用也是轮询,但是IO多路复用是交给内核进行各个socket的监控的。其次,由于NIO多次调用read这种系统调用,因此会频繁造成用户态和内核态的转换,而IO多路复用则是先调用select这个系统调用去查询是否有数据就绪的socket,然后有数据就绪,才调用read这个系统调用来读。所以从性能上来说,IO多路复用会比NIO好。

2. Redis6.0 之前为什么不使用多线程?

  • 单线程编程容易并且更容易维护;
  • Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
  • 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。

3. Redis6.0 之后为何引入了多线程?

引入多线程主要是为了提高网络 IO 读写性能
Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要设置IO线程数 > 1,需要修改 redis 配置文件 redis.conf :

四、内存管理

1. Redis 给缓存数据设置过期时间有啥用?

因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。

  • 常用指令:
    设置过期时间的命令 setex 外,其他方法都需要依靠 expire 命令来设置过期时间 。另外, persist 命令可以移除一个键的过期时间
  • 应用场景
    短信验证码、用户登录的 token

2. Redis如何判断数据是否过期?

Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间
过期字典的指向 Redis 数据库中的某个 key(键)过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。

3. 过期的数据的删除策略

  • 惰性删除只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
  • 定期删除每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。

4. 内存淘汰机制

  • 引入原因
    仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。
  • 淘汰策略(6+2,4.0版本后增加了两种)
    volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
    volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
    volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
    allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
    allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
    volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
    allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

五、Redis持久化机制

0. 面试答题模板

  • Redis 的话,它其实提供了两种持久化数据的方法,一种是AOF,一种是RDB。
  • 然后 AOF 的话它是一种,就是说每一条操作信息它都会进行追加记录这样的一种持久化的方式。当那个数据库重新启动的时候,它就会根据 AOF里面记录的数据操作,然后来进行一个数据库内容的重建。
  • 而 RDB 的话,它是做快照,也就是说在数据库运行的过程中,它可能会另开一个 IO的线程来进行数据库的快照记录,这样子的话来记录它某一个时间段的数据情况,这样子它进行恢复,数据库再次启动的时候就可以直接根据 RDB文件来进行恢复这两个操作。
  • 这样一执行的话就可以看出来, AOF的话,它虽然就是在执行的过程中性能的损耗是小的,但是如果数据库要进行重新启动的话,那它需要的耗时是比较长的。
  • RDB的话,它虽然重新启动的耗时小,但是说它在过程中会有一定的性能损耗。而且如果是在两个快照创建的中间就是数据库宕机,或者是这样子没有做成快照的话,会造成一部分数据的缺失。

1. 怎么保证 Redis 挂掉之后再重启数据可以进行恢复?

需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。

2. 什么是RDB持久化

指定的时间间隔内将内存中的数据集快照入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
在这里插入图片描述

3. RDB 创建快照时会阻塞主线程吗?

Redis 提供了两个命令来生成 RDB 快照文件:

  • save : 主线程执行,会阻塞主线程;
  • bgsave : 子线程执行,不会阻塞主线程,默认选项

4. 什么是AOF持久化?

以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录)只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

5. AOF 日志是如何实现的?

在这里插入图片描述

  • 与mysql的区别
    关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复),而 Redis AOF 持久化机制是在执行完命令之后再记录日志。
  • 为什么是在执行完命令之后记录日志呢?
    避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
    在命令执行完之后再记录,不会阻塞当前的命令执行。
  • 风险
    如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
    可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)

6. AOF 重写了解吗?

当 AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。
在这里插入图片描述

7. 如何选择 RDB 和 AOF?

  • RDB优点
  • RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小适合做数据的备份,灾难恢复。 虽然说AOF有重写机制,但是Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。
  • 使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快。
  • AOF优点
  • RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比繁重的, 虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程,但会对机器的 CPU 资源和内存资源产生影响,严重的情况下甚至会直接把 Redis 服务干宕机。AOF 支持秒级数据丢失(取决 fsync 策略,如果是 everysec,最多丢失 1 秒的数据),仅仅是追加命令到 AOF 文件,操作轻量
  • RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。
  • AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析,你也可以直接操作 AOF 文件来解决一些问题。比如,如果执行FLUSHALL命令意外地刷新了所有内容后,只要 AOF 文件没有被重写,删除最新命令并重启即可恢复之前的状态。

8. Redis 4.0 对于持久化机制做了什么优化?

Redis 4.0 **开始支持 RDB 和 AOF 的混合持久化(**默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

六、Redis事务

1. 如何使用事务?

在这里插入图片描述

  • Multi、Exec、discard
    从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
    组队的过程中可以通过discard来放弃组队。
  • WATCH
  • 在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
    redis使用的是乐观锁,先用watch监视key,然后分别用multi exec进行操作

2. 事务的错误处理

组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚

3. Redis 支持原子性吗?

Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的(而且不满足持久性)。
你可以将 Redis 中的事务就理解为 :Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断

  • 关系型数据库的事务
  • 原子性A
    事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 隔离性I
    并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  • 持久性D
    一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
  • 一致性C
    执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;

4. 如何解决Redis的事务缺陷

Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。

七、Redis性能优化

1. Bigkey

  • 定义
    如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。
  • 危害
    消耗更多的内存空间,影响性能
  • 如何发现
    使用 Redis 自带的 --bigkeys 参数来查找。
    通过分析 RDB 文件来找出 big key。这种方案的前提是你的 Redis 采用的是 RDB 持久化。

2. 大量key集中过期

  • 危害
    如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢。
  • 解决
    给 key 设置随机过期时间。
    开启 lazy-free(惰性删除/延迟释放)

八、Redis生产问题

1. 缓存穿透

  • 定义
    大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
    在这里插入图片描述
  • 解决方案
    1. 缓存无效 key
    如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,这种方式可以解决请求的 key 变化不频繁的情况
    2. 布隆过滤器
    布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据

    在这里插入图片描述
    布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

2. 缓存击穿

  • 定义
    请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中
    在这里插入图片描述
  • 解决方案
  • 设置热点数据永不过期或者过期时间比较长
  • 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
  • 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力

3. 缓存雪崩

  • 定义
    缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上
    在这里插入图片描述

  • 解决方案

  • 设置不同的失效时间比如随机设置缓存的失效时间。

  • 缓存永不失效(不太推荐,实用性太差)。

  • 设置二级缓存。

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

Redis数据一致性

4.1 一致性

在分布式系统中,一致性指多副本问题中的数据一致性

  • 强一致性
    当更新操作完成后,任何多个后续进程或者线程的访问都会返回最新的更新过的值
  • 弱一致性
    系统并不保证后续进程或者线程的访问都会返回最新的更新过的值
  • 最终一致性
    系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值

4.2 四种方案

先更新数据库,后更新缓存
先更新缓存,后更新数据库
先更新数据库,后删除缓存
先删除缓存,后更新数据库

4.3 更新缓存vs删除缓存

  • 更新缓存
    优点:每次数据变化都及时更新缓存,所以查询时不容易出现未命中的情况。
    缺点:更新缓存的消耗比较大。如果数据需要经过复杂的计算再写入缓存,那么频繁的更新缓存,就会影响服务器的性能。如果是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取该数据。

  • 删除缓存
    优点:操作简单,无论更新操作是否复杂,都是将缓存中的数据直接删除。
    缺点:删除缓存后,下一次查询缓存会出现未命中,这时需要重新读取一次数据库。从上面的比较来看,一般情况下,删除缓存是更优的方案。

4.4 先删除缓存再更新数据库

  • 出现失败

无论上面我们采用哪种方式去同步缓存与数据库,在第二步出现失败的时候,都建议采用重试机制解决

  1. 线程A更新数据库成功,线程A删除缓存失败;
  2. 线程B读取缓存成功,由于缓存删除失败,所以线程B读取到的是缓存中旧的数据。
  3. 最后线程A删除缓存成功,有别的线程访问缓存同样的数据,与数据库中的数据是一样。
  4. 最终,缓存和数据库的数据是一致的,但是会有一些线程读到旧的数据。
    解决方案**:延时双删**,为保证A操作已完成,基本思路如下
    删除缓存;
    更新数据库;
    sleep N毫秒;
    再次删除缓存
  • 没出现失败
    在这里插入图片描述
    线程A删除缓存成功;
    线程B读取缓存失败;
    线程B读取数据库成功,得到旧的数据;
    线程B将旧的数据成功地更新到了缓存;
    线程A将新的数据成功地更新到数据库。
    结果:进程A的两步操作均成功,但由于存在并发,在这两步之间,进程B访问了缓存。最终结果是,缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致。

4.5 先更新数据库后删除缓存

  • 出现失败
    线程A更新数据库成功,线程A删除缓存失败;
    线程B读取缓存成功,由于缓存删除失败,所以线程B读取到的是缓存中旧的数据。
    最后线程A删除缓存成功,有别的线程访问缓存同样的数据,与数据库中的数据是一样。
    最终,缓存和数据库的数据是一致的,但是会有一些线程读到旧的数据。
  • 没出现失败
    在这里插入图片描述
    线程A删除缓存成功;
    线程B读取缓存失败;
    线程B读取数据库成功,得到旧的数据;
    线程B将旧的数据成功地更新到了缓存;
    线程A将新的数据成功地更新到数据库。
    可见,进程A的两步操作均成功,但由于存在并发,在这两步之间,进程B访问了缓存。最终结果是,缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致。

九、Redis的集群

1. Redis集群实现方案

  • 原因
    单机版缺少数据可靠性,性能差
    主从模式
    哨兵模式
    自研
    cluster

2. 主从模式

在这里插入图片描述

  • 优点
    高可靠性:master数据库出现故障后,可以切换到slave数据库
    读写分离:Master以写为主,Slave以读为主
  • 缺点
    不具备自动容错和恢复能力,主节点故障,从节点需要手动升为主节点,可用性低

3. 哨兵模式

在这里插入图片描述

  • 作用
  • 监控所有服务器是否正常运行。监控主、从服务器,哨兵之间也互相监控
  • 故障切换。若检测到master宕机,会自动将slave切换为master,然后通过发布订阅的方式告知其他slave,切换master。同时旧的master变成从机,且在恢复之后不会恢复原来的主身份
    相对主从模式,多了个竞选机制.最大的问题就是浪费资源
  • 优点
    解决主从模式master故障不可以自动切换故障的问题
  • 缺点
  • 浪费资源,主从同步严重性能
  • 主机宕机后,投票选举结束之前,还是没有主机,此时Redis会禁止写操作
  • 只有一个master执行写操作,影响性能

4. Redis自研

  • 原因
    Redis全量存储,浪费资源。在Redis提供Redis Clouder之前,很多公司自行研发了很多集群方案
  • 客户端切片在这里插入图片描述

-

  • 代理切片
    在这里插入图片描述

在这里插入图片描述

5. Redis Cluster

添加链接描述

5.1 结构

在这里插入图片描述
采用多主多从,每一个分区都是由一个Redis主机和多个从机组成,片区和片区之间是相互平行的。Redis Cluster集群采用了P2P的模式,完全去中心化。
在这里插入图片描述

5.2 一致性哈希 (与hash)

一致性hash是一个0到2的32次方的闭合环型结构
在这里插入图片描述
系统的每个节点分配一个token,范围一般在0 ~ 2^{32} ,这些toke构成一个哈希环,数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。
在这里插入图片描述

5.3 1.Redis Cluster是如何将数据分片的?----哈希槽Slot 虚拟槽(哈希槽)

答题公式:多少个槽+原理+节点对槽的分配+优势
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
master节点的slave节点不分配槽,只拥有读权限
集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么节点 A 包含 0 到 5500号哈希槽,节点 B 包含5501 到 11000 号哈希槽,节点 C 包含11001 到 16384号哈希槽。
在这里插入图片描述
在这里插入图片描述

  • 最大的优势

在这里插入图片描述
一致性哈希增加和移除节点需要rehash

5.4 Redis如何保证高可用

Redis Cluster保证高可用主要还是依靠:故障检测与故障转移两种策略

6. 主从架构数据丢失

  • 异步复制同步丢失
    Redis主节点和从节点之间的复制是异步的,当主节点的数据未完全复制到从节点时发送了宕机,那么Master内存中的数据会丢失。
    开启主节点持久化能否解决?
    不能,master发生宕机后,由哨兵机制将重新选举新的主节点,如果旧的主节点在故障恢复后重启,那么此时它需要同步新的主节点的数据,此时新的主节点数据为空(假设这段时间没有数据写入),那么旧主机中的数据就会被刷新掉,造成数据丢失。
  • 集群产生脑裂
    集群脑裂是指一个集群中有多个主节点。
    在这里插入图片描述
  • 解决方案
    在Redis的配置文件中有参数如下:
min-slaves-to-writer 1
min-slaves-max-lag 10

表示至少有1个slave的与master的同步复制延迟不能超过10s。因此可以降低min-slaves-max-lag参数的值

7. 主从复制的过程

  • 设置服务器的地址和端口号
  • 建立套接字连接(建立主从服务器之间的联系)
  • 发送Ping命令(检验套接字是否可用)
  • 身份验证
  • 同步(主库向从库同步数据,分为全量复制和增量复制)
  • 命令传播(数据同步阶段完成后,主节点进入命令传播阶段,在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性)
    在这里插入图片描述

8. Redis集群的最大节点个数

16384

  • 网络通信开销的平衡
    在这里插入图片描述
    心跳包每秒都需要把当前节点的信息同步给集群中的其他节点。理论上,采用CRC16算法可以分配65535个槽,8k的心跳包;采用16384,需要2k的心跳包。而采用8k的心跳包效果一般,没必要采用如此大的心跳包
  • 集群规模的限制
    redis cluster不可能扩展到1000个主节点,太多主节点会导致网络拥堵
  • 16384可以确保每个master节点都有足够的插槽

十、分布式锁

1. 什么是分布式锁

分布式锁是为了保证在分布式场景下,共享资源在同一时刻只能被同一个线程访问,或者说是用来控制分布式系统之间同步访问共享资源。
在这里插入图片描述

2. 分布式锁有哪些特性

互斥性: 在任意时刻,同一条数据只能被一台机器上的一个线程执行
高可用性: 当部分节点宕机后,客户端仍可以正常地获取锁和释放锁
独占性: 加锁和解锁必须同一台服务器执行,不能再同一个服务器上加锁,在另外一个服务器上释放锁
防锁超时: 如果客户端没有主动释放锁,服务器会在一定时间后自动释放锁,防止客户端宕机或者网络异常宕机

3. 分布式锁的实现方式

基于关系型数据库
基于Redis
基于zookeeper

4. Redis如何实现分布式锁

SETNX该命令的作用是当key不存在时设置key的值,当key存在时,什么都不做
在这里插入图片描述
存在的问题:如果执行到执行业务时,业务代码突然出现异常,无法进行删除锁这一步,就会死锁

  • 改进一
    try-catch-finally 把删除锁的操作放到finally代码中 (缺点:如果Redis在执行业务时宕机,finally也不会执行)
  • 改进二
    给锁设置过期时间(缺点:无法保证加锁和解锁在同一台服务器)
    在这里插入图片描述
  • 改进三
    在这里插入图片描述
  • 改进四
    在这里插入图片描述
  • 改进五
    在这里插入图片描述
  • 改进六
    在这里插入图片描述
下面是一个基于sw::redis::RedisCluster和libevent异步订阅消息的示例代码: ```cpp #include <iostream> #include <string> #include <sw/redis++/redis++.h> #include <event2/event.h> using namespace std; using namespace sw::redis; void eventCallback(evutil_socket_t fd, short what, void *arg) { RedisCluster *redis = (RedisCluster *)arg; redis->cluster_recv(); } int main() { const string redis_cluster_address = "tcp://127.0.0.1:7000"; const string channel_name = "test_channel"; // 创建 RedisCluster 实例 auto redis = RedisCluster::create(); redis->connect(redis_cluster_address); // 订阅频道 auto callback = [](const string &channel, const string &msg) { cout << "Received message from channel " << channel << ": " << msg << endl; }; auto sub = redis->subscribe(channel_name, callback); // 创建 libevent 实例 auto event_base = event_base_new(); auto event = event_new(event_base, sub->fd(), EV_READ | EV_PERSIST, eventCallback, redis.get()); // 添加事件监听 event_add(event, nullptr); // 进入事件循环 event_base_dispatch(event_base); return 0; } ``` 这个示例代码中,首先创建了一个 RedisCluster 实例,然后调用其 connect 方法连接 Redis 集群。接着,调用 subscribe 方法订阅指定的频道,并传入一个回调函数来处理接收到的消息。 然后,创建了一个 libevent 实例,并使用 event_new 函数创建一个事件对象,将其绑定到 RedisCluster 实例的 socket 描述符上,并传入一个回调函数。最后,调用 event_base_dispatch 进入事件循环。 在事件循环中,libevent 会监听 Redis 集群返回的消息,当有消息到达时,会触发事件回调函数 eventCallback,在回调函数中调用 RedisCluster 实例的 cluster_recv 方法来处理接收到的消息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值