• Redis经典面试题


    Redis经典面试题

    问题1: Redis为什么这么快?

    1.1 基于内存实现

    Redis的数据都是存放在内存中,而像关系型数据库Mysql的数据存放在磁盘。访问磁盘数据是要进行网络IO连接,是很耗时的,而内存的数据访问和操作是相当快的。

    1.2 高效的数据结构

    我们都知道,mysql为了提高效率,采用了B+树的数据结构,对于一个应用场景来说合理的数据结构能够性能更好。我们来看看Redis的数据结构-内部编码。

    String :动态字符串SDS
    List :双端链表LinkedList + 压缩链表zipList。
    Hash:压缩链表zipList + 字典hashTable。
    Set:字典hashTable。
    ZSet:压缩链表zipList + 跳跃表skipList。

    SDS简单动态字符串

    image

    • 字符串长度处理:Redis获取字符串长度的复杂度 O(1),在C语言获取字符串长度从头开始遍历,复杂度为O(N).
    • 空间预分配:字符串频繁的修改,内存分配就越频繁,就会消耗性能,而SDS修改和空间扩展,会额外分配未使用的空间,减少性能消耗。
    • 惰性空间释放: SDS缩短时,不是回收多余的内存空间,而是free记录下多余的空间,后续有变更,直接使用free记录的空间,减少内存的肥分配。
    • 二进制安全:Redis可以记录二进制的数据,在C语言中字符串遇到’\0’会结束,而SDS中标志字符串结束的是len属性。

    字典
    Redis作为K-V型内存数据库,所有的键值就是用字典来存储。字典就是哈希表,比如HashMap,通过key就可以找到对应的value。哈希表的特性就是,在O(1)时间复杂度下就可以获得对应的值。

    跳跃表
    image

    • 跳跃表就是在链表的基础上,增加多级索引来提升查询效率
    • 跳跃表支持平均O(logN) 到 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。

    1.3 合适的数据编码

    Redis支持多种数据机构,每种基本类型,对应多种数据结构。什么时候使用什么样的数据结构,使用什么样的编码,是redis设计者总结优化的结果。

    1.4 合理的线程模型

    I/O多路复用

    I/O多路复用技术可以让单个线程高效的处理多个连接请求,而Redis将epoll作为IO多路复用的技术实现。并且,Redis的自身事件处理模型将epoll的连接,读写,关闭都转化为事件,不在网络I/O上浪费过多时间。

    I/O:网络IO
    多路:多个网络连接
    复用:复用同一个线程
    I/O多路复用,就是一个同步IO模型,它实现了一个线程可以监听多个文件句柄;一旦文件句柄就绪,就会通知应用程序进行相对于的读写操作,当没有文件句柄就绪时,就会阻塞应用程序,交出CPU。

    单线程

    Redis是使用单线程来进行指令操作的,避免了CPU不必要的上下文切换和锁的竞争的损耗,并且是基于内存操作数据,以达到最快的速度。

    Redis6.0版本引入了多线程的概念,但一般是网络相关的指令等IO操作时,会新开一个线程去执行,而执行命令操作数据等指令还是单线程去串行执行,仍是线程安全的。

    这样做的目的是因为redis的性能瓶颈在于网络IO而不是CPU,使用多线程提升IO读写的效率,从而提高整个Redis的性能。

    1.5 虚拟内存机制

    Redis直接自己构建了VM机制,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。

    Redis的虚拟内存机制是什么?

    虚拟内存机制就是暂时把不经常使用的数据(冷数据)从内存交换到磁盘,从而腾出宝贵的内存空间以供其他需要访问的数据(热数据)。通过VM机制可以实现冷热数据分离,使热数据在内存中,冷数据保存在磁盘中。这样就可以避免因内存不足而造成访问速度下降的问题。

    问题二:Mysql和Redis如何保证读写一致

    读取缓存步骤一般没有什么问题,但是一旦遇到数据的更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(Mysql)间的数据一致性问题了。

    不管是先更新数据库DB,再删除Redis缓存;还是先删除Redis缓存,再更新数据库DB,都有可能出现数据不一致的情况。

    举个栗子:
    1,如果删除了Redis缓存,还没有来得及更新数据库DB,另一个线程来读取,发现缓存为空,则去读取数据库的旧值,并写入缓存,此时缓存中为脏数据。
    2,如果先更新了数据库DB,在删除缓存前,出现了异常,导致没有删除掉缓存,则也会出现数据不一致。

    因为读和写是并发的,没法保证顺序性,就会出现数据库和缓存数据不一致的情况。

    目前有三个解决方案供参考。

    • 缓存延时双删
    • 删除缓存重试机制
    • 读取binlog异步删除缓存

    缓存延时双删:

    延时双删流程:
    1,写请求;
    2,删除缓存;
    3,更新DB;
    4,休眠一会,删除缓存;

    这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒,这样的目的是确保读请求结束,写请求可以删除读请求产生的脏数据。

    这个方案还可以,但是如果第二次删除缓存失败了呢?缓存和数据库的数据还是不一致。那么是否可以加上过期时间呢?业务接受在这过期时间内的数据不一致吗?还是有其他更好的方案呢?

    删除缓存重试机制

    image

    删除缓存重试机制流程:
    1,写请求更新数据库;
    2,某种原因删除缓存key失败
    3,将删除缓存失败的key丢到消息队列;
    4,消费消息队列的key,获取要删除的key;
    5,重试删除缓存操作;

    这个方案还不错,最终能够保证缓存和数据库的一致性。然而有一个缺点,会对业务代码造成大量入侵。

    读取binlog异步删除缓存

    image

    binlog是所有数据库表结构变更,以及表数据修改的二进制日志文件。
    binlog有两个常用的使用场景:

    • 主从复制:mysql replication在master端开启binlog,master把它的二进制文件传递给slaves来达到master-slave数据一致性的目的。
    • 数据恢复:通过mysqlbinlog工具来恢复数据。

    以mysql为例:
    1,可以使用阿里的canal将binlog日志采集发送到MQ消息队列;
    2,然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性。
    大啊是的大啊是的

    问题三:Redis的Hash冲突了怎么办

    Redis作为一个K-V型的内存数据库,它使用用一张全局的哈希表来保存所有的键值对。这张哈希表,由很多个哈希桶组成,每个哈希桶中的entry元素保存了key和value指针,其中*key指向了实际的健, *value指向了实际的值。
    image

    哈希表的查找效率很快,有点类似于java中的HashMap,它能够在O(1)时间复杂度快速找到键值对。首先通过key计算哈希值,通过哈希值找到哈希桶的位置,定位到entry,从entry中找到对应的数据。

    什么是哈希冲突?

    哈希冲突:通过不同的key,计算出的哈希值一样,导致落到同一个哈希桶中。

    Redis为了解决哈希冲突,采用了链式哈希。链式哈希就是同一个哈希桶中的多个元素,用链表来保存,它们之间依次用指针来连接。

    可能会有人有疑问,哈希冲突链上的元素只能通过指针逐一查找,当往哈希表插入更多的数据,那么发生哈希冲突的可能性也就越高,冲突链表也就越长,那查询效率不就是很低?

    为了保持高效,Redis会对哈希表做rehash操作,也就是增加哈希桶,减少冲突。为了rehash更高效,Redis默认使用两张全局哈希表,一个用于当前使用,称为主哈希表,另一个用作扩容使用,称为备用哈希表。

    值得注意的是,rehash的过程不是一次性的,这样会造成redis阻塞,无法提供服务,而是采用了渐进式rehash。

    问题四:在生成RDB期间,Redis可以同时处理读写请求吗

    可以的,Redis提供两个指令生成RDB文件,分别是save和bgsave。

    • 如果是save,会阻塞,因为是主线程执行的。
    • 如果是bgsave,会fork出一个子进程来生成RDB文件,主线程可以继续处理客户端的请求。

    问题五:什么是布隆过滤器

    定义:

    布隆过滤器是一种占用空间很小的数据结构,它由一个很长的二进制向量和一组Hash映射函数组成,它用于检索一个元素是否在一个集合中,空间效率和查询时间比一般的算法要好得多,缺点是有一定的误识别率和删除困难。

    原理:

    布隆过滤器的原理是:假设我们有集合A,A中有n个元素。利用k个哈希散列函数,将A中的每个元素映射到一个长度为a位的数组B中的不同位置上,这些被映射的位置上的二进制数均设置为1。如果待检查的元素,经过这k个哈希散列函数的映射后,发现其k个位置上的二进制数全部为1,这个元素很可能属于A,反之,一定不属于集合A。
    (不理解的反复读5遍)

    举个栗子:

    假设集合A有3个元素,{d1,d2,d3}。有一个哈希函数为Hash1。现在将集合A的每个元素都映射到一个长度为16的数组B。这个数组只存0和1,默认为0.
    现在我们将d1映射过来,假设Hash1(d1)= 2,我们就把数组B中,下标为2的格子改为1。
    现在将d2也映射过来,假设Hash1(d2)= 5,我们就把数组B中,下标为5的格子改为1。
    现在将d3也映射过来,假设hash1(d3)= 2,我们就把数组B中,下标为2的格子改为1。
    目前Hash1(d1)和Hash(d3)都等于2。

    因此,我们要确认一个元素dn是否在集合A中,只要算出Hash1(dn)得到的索引下标,只要是0,那么元素dn肯定不在集合A中。如果索引下标是1呢?那该元素可能是集合A的某个元素。因为,d1和d3得到的下标值都是1,还有可能是其他元素映射的,布隆过滤器是存在这个缺点的:会存在哈希碰撞的假阳性,判断存在误差。

    如何减少误差:

    • 多几个哈希函数映射,降低哈希碰撞的概率。
    • 增加数组的长度,可以增大哈希函数生成的数据范围,也能降低哈希碰撞的概率。

    即使存在误差,我们发现,布隆过滤器没有存储完整的数据,而是通过一系列的哈希函数找出位置,并填充二进制向量。如果数据量很大,布隆过滤器通过极少的错误率,换取了存储空间的极大节省,还是较为划算的。

    实现布隆过滤器的开源类库:

    目前布隆过滤器已经有实现的类库了,比如Google的Guava类库,Twitter的Algebird类库,或者基于Redis自带的Bitmaps自行实现设计也是可以的。

    问题六:Redis的过期key的删除策略

    Reids中,我们可以设置缓存key的过期时间,Redis的过期策略是指,当Reids中缓存的key过期了,Redis如何处理。
    过期策略有以下三种:

    • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期时间的数据,对内存友好;但是会占用大量的CPU的资源来处理过期的数据,从而影响缓存的响应时间和吞吐量。
    • 惰性过期:只有当访问一个key时,才去判断该key是否过期,过期则清除。该策略最大化的节省了CPU资源,却对内存非常不友好。极端情况下会出现,大量的过期key没有被访问过,从而这些key不会被清除,占用大量内存。
    • 定期过期:每隔一定的时间,会去扫描数据库中expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案,通过调整定期扫描的时间间隔和每次扫描的限定数量,可以在不同情况下使用CPU和内存资源达到最优的平衡效果。(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向健空间的某个健的指针,value是该健的毫秒精度的UNIX时间戳表示的过期时间。)

    Redis同时使用了惰性过期和定期过期两种过期策略。

    问题七:Redis的内存淘汰机制

    Redis的内存淘汰机制和过期ky的删除策略不要搞混淆了。内存淘汰机制是当Redis的内存空间不够了,设置某一种策略来处理数据。

    Redis4.0之后内存淘汰机制一共有8种。

    • noeviction:不会淘汰任何数据,当使用空间超过maxmemory时,新增的数据会直接报错,Redis默认的淘汰机制。
    • volatile-ttl: 优先淘汰过期时间快到的键值。
    • volatile-random: 从设置了过期时间的健中随机淘汰。
    • volatile-lru: 从设置了过期时间的健中使用LRU算法,最近最少使用的健淘汰。
    • volatile-lfu: 从设置来过期时间的健中使用LFU算法,最近最少使用的健淘汰。
    • allkeys-random: 从所有的健中随机淘汰。
    • allkeys-lru: 从所有的健中使用LRU算法,最近最少使用的健淘汰。
    • allkeys-lfu: 从所有的健中使用LFU算法,最近最少使用的健淘汰。

    根据系统的不同业务场景,选择合适的淘汰机制。

    关于Redis实现的LRU和LFU算法的详细内容可以查阅这篇文章。
    参考文档:https://wenku.baidu.com/view/85b703173a68011ca300a6c30c2259010202f3fa.html

    问题八:怎么实现Redis的高可用

    在实际项目中使用Redis,肯定不会单点部署Redis服务的,因为单点部署一旦宕机,就不可用了。为了实现高可用,通常是部署多台Redis。
    Redis主要有三种部署模式:

    • 主从模式
    • 哨兵模式
    • 集群(Cluster)模式

    8.1 主从模式

    主从模式中,Redis部署来多台机器,由主节点master,负责读写操作,由从节点slave,只负责读操作。从节点的数据来自主节点,实现的原理就是主从复制机制。

    主从复制包括全量复制增量复制和两种。

    一般当slave节点第一次链接master节点,会采用全量复制。
    slave和master全量复制后,当master上的数据发生变更时,采用增量复制。

    8.2 哨兵模式

    主从模式中,一旦主节点由于故障不能提供服务,需要人工将从节点切换为主节点,同时还要通知应用方更新主节点地址。显然,多数业务场景都不能接受这种故障处理方式。Redis从2.8版本开始正式提供了哨兵模式来解决此问题。

    哨兵模式,由一个或多个Sentinel组成的Sentinel系统,它可以监视所有的Redis主节点和从节点,并在被监视的主节点进入下线状态后,自动将下线的主节点所属的从节点升级为新的主节点。但是一个哨兵进程对Redis节点监视,就可能出现单点问题,因此,一般使用多个哨兵来监控Redis节点,并且各个哨兵之间还会进行监控。

    哨兵模式的主要功能有:

    • 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
    • 自动故障转移(Automatic Failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
    • 配置提供者(Configuration Provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
    • 通知(Notification):哨兵可以将故障转移的结果发送给客户端。

    8.3 集群(Cluster)模式

    哨兵模式基于主从复制模式,实现读写分离,而且可以故障自动转移,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。因此Cluster集群模式应用而是,它是在Redis3.0推出的,实现了分布式存储。对数据进行分片,也就是每台Redis实例存储了不同的数据,来解决在线扩容的问题,并且也提供了主从复制和故障转移的功能。但分布式存储也带了其他但问题,在此不展开描述。

    关于主从复制模式,哨兵模式,Cluster集群模式的更多详细内容,查看另外的单独文章。

  • 相关阅读:
    95-Java的对象序列化、反序列化
    Codeforces Round #814 (Div. 2) A.B.C
    全国A级旅游景区清单数据(2023年更新)
    PLC中ST编程的起保停
    Kafka(二)- Kafka集群部署
    7.Nodejs新特性async和await的使用
    【C++】深度解剖多态
    SGI图像文件格式
    JavaScript之变量、数据类型、数据转换、模板字符串
    计网第五章(运输层)(八)(TCP的连接释放)
  • 原文地址:https://blog.csdn.net/weixin_44143114/article/details/126613048