Redis常见面试题

  • 简述一下Redis(为什么要选用Redis)

    • Redis是一个开源的使用C语言编写,可基于内存、可持久化的Key-Value数据库,和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(有序集合)和hash(哈希类型)

    • 优势在于

      • 速度快,性能极高、可持久化、丰富的数据类型、支持数据的备份

    • Redis和Memcached区别

      • 1、类型

        • Redis是一个开源的内存数据结构存储系统,用作数据库、缓存和消息代理(分布式锁)

        • Memcached是一个免费的开源高性能分布式内存对象缓存系统,它通过减少数据库负载来加速动态Web应用程序

      • 2、数据结构

        • Redis支持字符串、散列、列表、集合、有序集、位图、超级日志和空间索引,而Memcached支持字符串和整数

      • 3、执行速度

        • Memcached(多线程)的读写速度高于Redis

      • 4、主从复制

        • Memcached不支持复制,而Redis支持主从复制,允许从属Redis服务器成为主服务器的精确副本;来自任何Redis服务器的数据都可以复制到任意数量的从属服务器

      • 5、密钥长度

        • Redis的密钥长度最大为2GB,而Memcached的密钥长度最大为250字节

      • 6、线程

        • Redis是单线程,而Memcached是多线程的

  • 为什么说Redis块或者性能高

    • 1、完全基于内存,数据存在内存中(不需要进行磁盘IO,读写速度快),类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)

    • 2、数据结构专门设计(Key-value,在O(1)时间内找到value),比如SDS结构中字符串长度len,压缩链表等

    • 3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;(Redis在处理客户端的请求时,包括获取(socket读)、解析、执行、内容返回(socket写)等都由一个顺序串行的主线程处理,这就是所谓的单线程,但如果严格来讲从Redis之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据,无用连接的释放,大Key的删除等等)

    • 4、使用多路I/O复用模型,非阻塞IO;多路I/O复用模型是利用select、poll、epoll可以同时监察多个流的I/O事件的能力,在空闲的时候会把当前线程阻塞掉,当有一个或者多个流有I/O操作时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发生了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作

    • 5、RESP(Redis的序列化协议)协议,文本协议,解析迅速,虽然浪费流量,这是折中吧

    • 6、持久化采用了子线程进行磁盘操作

  • 简述下Redis单线程模型(Redis原理)

    • Redis基于Peactor模式来设计开发了一套高效的事件处理模型,即Redis中的文本事件处理器(file event handler),由于文件事件处理器是单线程方式运行的,所以一般说Redis是单线程模型

    • 实现方式:

      • 1、I/O多路复用

        • 通过I/O多路复用来监听来自客户端的大量连接(或者说监听多个socket),将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。I/O多路复用技术的使用让Redis不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。

      • 2、基于事件驱动

        • Redis服务器是一个事件驱动程序,服务器需要处理两类事件,文件事件:时间事件。

        • 文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器,当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事情处理器来处理这些事件

        • 虽然文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好的与Redis服务器中其他同样以单线程方式运行的模块进行对接,保持了Redis内部单线程设计的简单性

        • 文件事件处理器(file event handler)主要包含4个部分:多个socket(客户端连接);IO多路复用程序(支持多个客户端连接的关键);文件事件分派器(将socket关联到相应的事件处理器);事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

  • Redis为什么要使用单线程而不是多线程

    • Redis6.0之前的版本真的是单线程,之后才有了多线程

    • 官方曾做过类似问题的回复,使用Redis时,几乎不存在CPU成为瓶颈的情况,Redis主要受限于内存和网络,例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求。所以如果应用程序主要使用O(N)或O(log(N))的命令,它们几乎不会占用太多CPU,使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统的复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模拟以及IO多路复用等技术,处理性能非常高,因此没必要使用多线程。单线程机制使得Redis内部实现的复杂度大大降低,hash的惰性Rehash、Lpush等等“线程不安全”的命令都可以以无锁实现

  • Redis6.0为什么要引入多线程呢

    • Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理80000到100000QPS,这也是Redis处理的极限了。对于80%的公司来说单线程的Redis以及足够使用了

    • 但随着越来越复杂的业务场景,有些公司打不动就上亿的交易量,因此需要更大的QPS,常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大,某些适用于单个Redis服务器的命令不适用数据分区,数据分区无法解决热点读/写问题,数据偏斜,重新分配和放大/缩小变得更加复杂等等

    • 从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的IO消耗,优化主要有两个方面:

      • 提高网络IO性能,典型的实现比如使用DPDK来替代内核网络栈的方式

      • 使用多线程充分利用多核,典型的实现比如Memcached。协议栈优化的这种方式跟Redis关系不大,支持多线程是一种最有效最便捷的操作方式。所以总结起来,Redis支持多线程主要就是两个原因:

        • 可以充分利用服务器CPU资源,目前主线程只能利用一个核

        • 多线程任务可以分摊Redis同步IO读写负荷

  • Redis的数据结构

    • 简单动态字符串SDS和C语言自带的字符串有什么不同

      • 1、常数复杂度获取字符串长度

        • 由于len属性的存在,我们获取SDS字符串的长度只需要读取len属性,时间复杂度为O(1),而对于C语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为

          O(n)

      • 2、避免缓冲区溢出

        • 对于SDS数据类型,在进行字符修改的时候,会首先根据记录的len属性检查内存空间是否满足需求,如果不满足会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出,而C语言自带的就会有

      • 3、减少修改字符串的内存重新分配次数

        • C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减少时会造成内存泄露

        • 而对于SDS,由于len属性和free属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略

          • 空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可减少连续执行字符串增长操作所需的内存重分配次数

          • 惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用free属性将这些字节的数量记录下来,等待后续的使用(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间)

      • 4、二进制安全

        • 因为C字符以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等等),内容可能包含空字符串,因此C字符无法正确存取;而所有SDS的API都是以处理二进制的方式来处理buf里面的元素,并且SDS不是以空字符串来判断是否结束,而是以len属性表示的长度来判断字符串是否结束

      • 5、兼容部分C字符串函数

        • 虽然SDS是二进制安全的,但是一样遵从每个字符串但是以空字符串结尾的惯例,这样可以重用C语言库《string.h》中的一部分函数,C字符串可以使用全部函数

           

    • Redis字典的底层实现与hashTable相关问题

      • 解决冲突:链地址法:和java的HashMap一样

      • 扩容:复制出另外一个hash表,并且会重新计算hash值,进行渐进式hash,这一点和java不同,特别是java8的不用重算hash机制的优化点

        • 什么叫渐进式hash?也就是说扩容和收缩操作不是一次性、集中完成的,而是分多次,渐进式完成的。如果保存在Redis中的键值对只有几个几十个,那么rehash操作可以瞬间完成,但是如果键值对有几百万,几千万,那么要一次性的进行rehash,势必会造成Redis一段时间内不能进行别的操作,所以Redis采用渐进式rehash,这样在进行渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上查找。但是进行增加操作一定是在新的哈希表上进行的

    • 压缩链表原理

      • 压缩链表(ziplist)是Redis为了节省内存而开发出的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值

    • zset底层跳表原理(为什么不选平衡树)

      • 本质就是多级链表,并有序;跳表是有序链表,但是我们知道,即使对于排过序的链表,我们对于查找还是需要进行通过链表的指针进行遍历的,时间复杂度很高,这个显然是不能接受的,是否可以像数组那样,通过二分法进行查找呢,但是由于在内存中的存储的不确定性,不能这样做,但是我们可以结合二分法的思想,跳表就是链表和二分法的结合,链表从头到尾都是有序的,可以进行跳跃查找(形如二分),降低时间复杂度

      • skiplist与平衡树、哈希表的比较

        • 1、skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的,因此,在哈希表上只能做单个key的查找,不适宜做范围查找,所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点

        • 2、在做范围查找的时候,平衡树比skiplist操作要复杂,在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点,而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第一层链表进行若干的遍历就可以实现

        • 3、平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速

        • 4、从内存占用上来说,skiplist比平衡树更灵活一些,一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均文1/(1-p),具体取决于参数p的大小。如果像Redis里实现的一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势

        • 5、查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持降低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些,所以我们平时使用的各种Map和dictionary结构,大都是基于哈希表实现的

        • 6、从算法实现难度来说,skiplist比平衡树要简单很多

  • 红黑树和跳表

    • 跳表就是带多级索引的链表,时间复杂度O(lgn),所以实现的功能和红黑树差不多,但是跳表有一个区间查找的优势,红黑树没有,所以redis底层就是用的跳表

    • 1、跳表(跳表以空间换取时间,来实现快速查找)

      • 从图中可以看到,跳表主要由以下部分构成:

        • 表头(head):负责维护跳跃表的节点指针

        • 跳跃表节点:保存着元素值,以及多个层

        • 层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于底层指针,为了提高查找效率,程序总是从高层开始访问,然后随着元素值范围的缩小,慢慢降低层次

        • 表尾:全部由NULL组成,表示跳跃表的末尾

    • 2、红黑树

      • 红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色,对于任何有效的红黑树增加了如下的额外要求:

        • 节点是红色或黑色

        • 根是黑色

        • 所有叶子都是黑色(叶子是NULL节点)

        • 每个红色节点必须有两个黑色的子节点(从每个叶子到根的所有路径上不能有两个连续的红色节点)

        • 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点

                 

  • Redis中过期策略和缓存淘汰机制

    • 1、策略分为:定期删除 + 惰性删除

      • 定期删除:redis默认每隔100ms检查,是否有过期的key,有过期的key则删除;需要说明的是,redis不是每隔>100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除

      • 惰性删除:获取某个key的时候,redis会检查一下,如果过期了就会删除

    • 2、redis内存淘汰机制

      • noeviction:不进行淘汰数据。一旦缓存被写满,再有写请求进来,redis就不再提供服务,而是直接返回错误

      • 在设置了过期时间的数据中进行淘汰:

        • volatile-ttl: 移除即将过期的键值对

        • volatile-random: 随机移除某个键值对

        • volatile-lru:移除最近最少使用的键值对

        • volatile-lfu:移除最近最不频繁使用的键值对

      • 在所有数据中进行淘汰:

        • allkeys-random:随机移除某个键值对

        • allkeys-lru:移除最近最少使用的键值对

        • allkeys-lfu:移除最近最不频繁使用的键值对

      • 通常情况下推荐优先使用allkeys-lru策略。这样可以充分利用LRU这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。如果业务数据中有明显的冷热数据区分,建议使用allkeys-lru策略。如果业务应用中的数据访问频率相差不大,没有明显的冷热数据分区,建议使用allkeys-random策略,随机选择淘汰数据

  • Redis中持久化机制

    • Redis的持久化有两种机制,一个是RDB,也就是快照,快照就是一次全量的备份,会把所有Redis的内存数据进行二进制的序列化存储到磁盘。(Redis会Fork一个子进程,快照的持久化就交给子进程去处理,而父进程继续处理线上业务的请求);另一种就是AOF日志,AOF日志记录的是数据操作修改的指令记录日志,可以类比MySQL的Binlog,AOF日期随着时间的推移只会无限增量

    • Redis4.0之后,引入了新的持久化模式,混合持久化,将RDB的文件和局部增量的AOF文件相结合。RDB可以使用相隔较长的时间保存策略,AOF不需要是全量日志,只需要保存前一次RDB存储开始到这段时间增量AOF日志即可,一般来说,这个日志量是非常小的

  • Redis集群的主从复制

    • 主从复制,是将主节点的数据,复制到从节点。且数据的复制是单向的,主数据库一般可以读写操作,从服务器只有读的权限,并接收主数据库同步过来的数据

    • 主从复制的原理:

      • 当一个数据库启动时,会向主数据库发送同步命令

      • 主数据库接收到同步命令后开始在后台保存快照(执行RDB操作),将接收到的命令缓存起来

      • 当快照完成后,redis会将快照文件和所有缓存的命令发送给从数据库

      • 从数据库收到后,会载入快照文件并执行收到的缓存的命令

    • 采用完整重同步和部分重同步两种模式

      • 完整同步用于处理初次复制的情况,通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区的写命令来进行同步

      • 部分重同步用于处理断线后重复制情况,当从服务器在断线后重新连接主服务器时,主服务器可以将主从服务器连接断开期间执行的命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态

  • 缓存雪崩和缓存穿透

    • 缓存雪崩:

      • 缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉

      • 解决办法:

        • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生

        • 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中

        • 设置热点数据永远不过期

        • 设置本地分布式缓存

    • 缓存穿透:

      • 缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为‘-1’的数据或id为特别大不存在的数据,这时的用户很可能是攻击者,攻击会导致数据库压力过大

      • 解决办法:

        • 接口层增加效验,如用户鉴权效验,id做基础效验,id<=0的直接拦截

        • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短些,如30秒,这样可以防止攻击用户反复用同一个id暴力攻击

        • 用布隆过滤器,用很低的代价计算出某个值是否真实存在

  • 缓存和数据库的数据一致性问题

    • 1、将不一致分为三种情况:

      • 数据库有数据,缓存没有数据

      • 数据库有数据,缓存也有数据,数据不相等

      • 数据库没有数据,缓存有数据

    • 2、缓存+数据库读写的模式

      • 首先尝试从缓存读取,读到数据则直接返回,如果读不到,就读数据库,并将数据写到缓存,并返回

      • 需要更新数据时,先更新数据库,然后把缓存里对应的数据失效掉(删除),

        • 更新为什么是删除呢?这个可以根据业务复杂度来衡量,也可以选择更新缓存

          • 对于第一种,在读数据的时候,会自动把数据库的数据写到缓存,因此不一致自动消除

          • 对于第二种,数据最终变成不相等,但他们之前在某一刻一定是相等的(不管你使用懒加载还是预加载的方式,在缓存那一刻他们两一定是相等的),这种不一致,一定是由于你更新数据所引发的,前面讲了更新数据的策略,先更新数据库,再删除缓存,因此不一致的原因,一定是数据库更新了,但是删除缓存失败了

          • 对于第三种,情况和第二种类似,你把数据库的数据删了,但是删除缓存的时候失败了

          • 因此最终的结论是:需要解决的不一致,产生的原因是更新数据库成功,但是删除缓存失败

      • 解决方案:

        • 对删除缓存进行重试,数据的一致性要求越高,越是重试得快

        • 定期全量更新,简单来说就是定期吧缓存全部清掉,然后再全量加载缓存

        • 给所有缓存一个失效时间

        • 常规的缓存和数据库设计思路

          • 并发不高的情况:

            • 读:读redis->没有,读mysql->把mysql数据写到redis,redis有的话直接从返回

            • 写:写mysql->成功,再写redis

          • 并发高的情况:

            • 读:读redis->没有,读mysql->把mysql数据写到redis,redis有的话直接从返回

            • 写:异步,先写入redis的缓存,就直接返回;定期或特定动作将数据保存到mysql,可以做到多次更新,一次保存 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值