- 收割Offer:互联网大厂面经
- 布兜编著
- 5649字
- 2024-12-28 12:51:50
1.3 Redis
Redis是由C语言开发的一款开源、高性能的键值对内存数据库,为了适应不同场景的存储需求,它支持字符串、列表、有序集合、散列和集合等多种数据类型。Redis内置了复制、RDB与AOF持久化、Lua脚本以及Cluster集群高可用解决方案。关于Redis的高频面试知识点整理如下:Redis的5种基本数据类型及底层数据结构实现、RDB与AOF持久化、主从复制原理、Cluster集群、哈希槽、过期策略、Gossip协议、重定向、Pipeline、Redis为什么这么快、Redis实现分布式锁、Redis与Memcache的区别等。
1.3.1 Redis的5种基本数据类型及对应底层实现
面试官提问
● Redis的5种基本数据类型是什么?
● 解释一下Redis的5种基本数据类型对应的底层数据结构以及切换条件。
● 你了解SDS吗,与C字符串相比它有什么优点?
● 压缩列表的连锁更新问题是怎么产生的?
● 说明Hash扩容、渐进式rehash过程。
● 说说跳表数据结构、一次查询的过程、时间复杂度。
Redis的5种基本数据类型是string、list、set、hash和sortedset。
表1-17给出了Redis的5种基本数据类型及其对应的底层实现。
表1-17 Redis的5种基本数据类型及其对应底层实现
1.Redis底层数据结构之SDS
与C字符串相比,SDS具有以下优点:
● 获取字符串长度的时间复杂度为O(1)。SDS使用len属性维护了本身的长度。
● 避免缓冲区溢出。SDS在修改时先检查空间是否满足需要,若不足则自动将SDS的空间扩展至修改所需的大小。
● 空间预分配与惰性释放减少内存重分配次数。
◆ 空间预分配:字符串增长操作需要对SDS空间进行扩展,若SDS修改后的长度小于1MB,则程序分配和len属性同样大小的未使用空间;若SDS修改后的长度大于或等于1MB,则程序只分配1MB的多余空间。
◆ 空间惰性释放:当需要缩短SDS保存的字符串时,程序不会立即通过内存重分配来回收字符串缩短后多余的空间,而是使用free属性标记可用空间大小等待将来使用。
● 二进制安全。SDS使用len属性的值而不是空字符来判断字符串是否结束。
2.Redis底层数据结构之压缩列表ziplist
一个压缩列表可以包含任意多个节点(entry),压缩列表各部分组成如图1-96所示。
图1-96 Redis底层数据结构之压缩列表
图1-96中,每个压缩列表节点都由previous_entry_length、encoding、content三个部分组成,previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节;如果前一节点的长度大于或等于254字节,那么previous_entry_length属性的长度为5字节。虽然压缩列表节省空间,但由于节点的变长,在插入或者更新时可能引起连锁更新问题。
3.Redis底层数据结构之双端链表linkedlist
双端链表的组成如图1-97所示。
图1-97 Redis底层数据结构之双端链表
链表实现的特性:双端无环,相对于压缩列表来说更浪费空间。
4.Redis底层数据结构之快表quicklist
quicklist是zipList和linkedList的结合体,它将linkedList按段切分,每一段就是一个zipList,多个zipList之间使用双向指针连接起来,如图1-98所示。快表是压缩列表和双端列表的折中方案。
图1-98 Redis底层数据结构之快表
5.Redis底层数据结构之整数集合intset
intset整数集合是一个有序数组,只升级不降级。整数集合的升级规则是,当向一个int16类型数组的整数集合添加一个int64类型的整数值时,整数集合所有元素都会被转换成int64类型。
6.Redis底层数据结构之哈希表hashtable
Redis的哈希表如图1-99所示。Redis的哈希表使用链地址法来解决冲突,哈希表中的键值对增加或者减少太多可能会触发rehash。如果执行的是扩容操作,那么新哈希表的大小为第一个大于或等于当前键值对数量×2的2^n(2的n次方幂);如果执行的是收缩操作,那么新哈希表的大小为第一个大于或等于当前键值对数量的2^n。rehash是渐进式完成的,rehash期间添加的键值对只会保存到新表,删除、查找、更新等操作会在两个哈希表上进行。每次对字典执行添加、删除、查找或者更新操作时,除了执行指定的操作以外,还会顺带将旧表里的部分键值对rehash到新表中。
图1-99 Redis底层数据结构之哈希表
7.Redis底层数据结构之跳表skiplist
跳表由多层组成,如图1-100所示,由下至上为1~L层,第k层是第k-1层的索引。每层都是一条有头节点的有序链表,第1层的链表包含跳表中的所有元素。如果某个元素在第k层出现,那么它在第1到k-1层一定会出现,在第k+1层则会按一定的概率出现。
图1-100 Redis底层数据结构之跳表
在查找元素时,从最顶层链表的头节点开始遍历。如果当前节点的下一个节点包含的值比目标元素小,则继续往右查找;否则就跳转到下一层查找。如上重复向右和向下的操作,直到找到与目标值相等的元素为止。图1-100中的加粗箭头标记出查找元素24的过程。
1.3.2 Redis为什么这么快
面试官提问
● Redis为什么这么快?
Redis速度快的原因有以下4点:
(1)Redis是一个键值对内存数据库。
(2)使用IO多路复用技术。
(3)非CPU密集型任务。对大key进行非O(1)时间复杂度的操作(CPU密集)会阻塞后续请求,Redis快的前提是不会出现类似情况。
(4)单线程的优势。避免了多线程上下文切换以及共享资源加锁的性能损耗。
1.3.3 Redis持久化之RDB与AOF
面试官提问
● Redis持久化有哪两种方式,它们有什么区别?
● 为什么需要AOF重写,怎么实现?
1.挺久化的两种方式
Redis是内存数据库,服务器宕机等情况会导致数据丢失,为此Redis提供了两种持久化方案。
1)RDB持久化
RDB持久化是将Redis内存快照保存到硬盘上,Redis可以通过RDB文件还原数据库当时的状态。RDB文件可以通过SAVE或者BGSAVE生成:
● SAVE:阻塞Redis服务进程,直至生成RDB文件。
● BGSAVE:复刻一个子进程来创建RDB文件。
2)AOF持久化
AOF持久化流程如图1-101所示。
● 命令追加:写命令追加到AOF缓冲区。
● 文件持久化:AOF缓冲区的内容根据对应的策略写入AOF文件。
● AOF重写:随着命令持续写入,AOF文件越来越大,重写AOF文件进行压缩。
● 数据恢复:当Redis重启时,可以加载AOF文件进行数据恢复。
图1-101 AOF持久化
3)RDB与AOF的区别
AOF数据同步实时、数据丢失少、磁盘IO开销大,数据恢复相对较慢;RDB快照文件尺寸小,数据恢复速度快,但RDB快照文件生成间隔一般较长,会丢失最后一次生成快照后的数据修改,同时Redis版本演进过程存在旧版本无法兼容新版RDB格式的问题。
2.AOF重写
AOF文件重写原理:从数据库中读取键值并用一条命令记录键值对,代替该键被修改的过程中产生的多条命令。如表1-18所示,list被修改5次,对应AOF文件保存了5条命令,AOF重写后,只需保存1条命令。
表1-18 AOF重写前后文件内容的变化
为了避免AOF重写阻塞主进程处理客户端的请求,一般复刻一个子进程完成AOF重写,由于子进程重写的同时Redis又接收客户端的写命令对当前数据库进行修改,因此可能导致数据库当前状态和重写后的AOF文件所保存的数据库状态不一致。为了解决该问题,Redis每执行一条写命令,就同时发送给AOF缓冲区和AOF重写缓冲区,如图1-102所示。
图1-102 AOF重写期间写命令同时发送给AOF缓冲区和AOF重写缓冲区
子进程完成AOF重写后向父进程发送一个信号,通知父进程完成以下工作:
● 将AOF重写缓冲区中的所有内容写入新的AOF文件。
● 重命名新旧AOF文件完成新旧文件的替换。
在整个AOF重写过程中,以上两个工作会对Redis主进程造成阻塞,如表1-19所示。
表1-19 AOF重写主进程阻塞时间点
1.3.4 Redis实现分布式锁的关键点
面试官提问
● 怎样实现一个Redis分布式锁,需要注意哪些问题?
● Redis实现的分布式锁与ZooKeeper实现的分布式锁有什么区别?
Redis锁主要利用Redis的setnx命令实现,setnx是SET if Not exists的简写。执行setnx key value,当键不存在时,将key的值设置为value,此时锁抢占成功。可以通过删除键值对或者过期时间来释放锁。实现Redis锁需要注意的事项如下:
1.避免死锁
设置key的过期时间,以保证即使锁没有被显式释放,也可以在一定时间后自动释放,避免资源被永远锁住。
2.锁续期
当前线程获取锁后执行任务,当任务耗时大于Redis key过期时间时,锁会被释放,会存在其他线程获取到该锁的可能。此时可以为已经获取锁的线程增加守护线程,对将要过期但未释放的锁延长有效时间。
3.只允许获取锁的线程释放锁
将参与抢锁的客户端id设置在value中(setnx key value),释放锁前校验value中存放的id是否为自己。
4.互斥性
Redis正常运行时执行setnx命令可以保证只允许一个客户端持有锁;当Redis发生主从切换时,key未及时同步到从节点,锁可能被其他客户端再一次获取,针对该场景可引入红锁机制。
5.可重入(可选)
若允许当前线程在持有锁的情况下再次请求加锁,那么这个锁就是可重入的。Redis可对锁进行重入计数,加锁时加1,解锁时减1,当计数归0时释放锁。
补充说明ZooKeeper与Redis实现分布式锁的异同:从CAP的角度来说,ZooKeeper因为有过半策略保证数据的强一致性,所以ZooKeeper实现的分布式锁强调的是CP,Redis实现的分布式锁强调的是AP。
1.3.5 Redis与Memcache的区别
面试官提问
● 请谈谈Redis与Memcache有什么区别。
Redis与Memcache的区别主要体现在以下4个方面:
(1)数据结构方面:Redis数据类型更丰富,有String、List、Set、Zset、Hash等,Memcache只支持String类型。
(2)数据持久化方面:Redis支持AOF与RDB两种持久化方案,而Memcache不支持持久化。
(3)高可用方面:Redis原生支持集群模式,而MC需要客户端去实现集群。
(4)线程模型方面:Redis使用单线程模型(高版本存在多线程),MC是多线程模型。
1.3.6 Redis主从复制原理之SYNC与PSYNC
面试官提问
● 谈谈Redis主从复制的原理。
● SYNC与PSYNC有什么区别?
Redis全量复制(SYNC)一般发生在从节点初始化阶段,这时主节点生成快照传递给从节点,从节点载入快照进行数据恢复。全量复制流程如图1-103所示。
步骤01 Slave向Master发送SYNC命令,要求全量复制。
步骤02 Master执行BGSAVE命令生成RDB快照文件,同时使用缓冲区记录之后的写命令。
步骤03 Master生成RDB文件后发送给Slave,该期间Master继续记录来自客户端的写命令。
步骤04 Slave收到RDB文件后丢弃旧数据,载入Master传递过来的快照。
步骤05 Master完成RDB文件传输后开始向Slave发送缓冲区积累的写命令。
步骤06 Slave完成RDB快照的载入后,开始执行来自Master缓冲区的写命令。
图1-103 主从复制SYNC
SYNC命令的典型使用场景如下:
(1)Slave第一次和Master连接,即初次复制。
(2)主从断连后重新建立连接。
初次复制使用SYNC命令进行全量复制是高效且必要的,但主从断连后重新建立连接,可能从节点仅丢失了几个写命令,这时也使用全量复制并不划算,如图1-104所示。
图1-104 断后重连全量复制
为了解决该问题,Redis 2.8版本提供了PSYNC命令实现部分复制,PSYNC命令格式如下:
PSYNC <runid> <offset>
其中runid代表主服务器id,offset表示从服务器最后接收命令的偏移量。
PSYNC命令执行流程如图1-105所示。
图1-105 PSYNC执行流程
步骤01 当前节点向Master发送SLAVEOF命令,请求成为Slave。
步骤02 当前节点根据自己是否保存Master runid来判断是否是第一次复制,若是第一次复制,则向Master发送psync ? -1命令来进行全量复制,否则向Master发送psync < runid > < offset >命令。
步骤03 Master接收到psync命令,若runid与本机的id一致,并且offset和本机的偏移量相差没有超过复制积压缓冲区大小(复制积压缓冲区缓存了Master传播出去的命令),则Master只需传回失去连接期间丢失的命令,即部分复制;否则Master返回FULLRESYNC<runid> < offset>命令,Slave将runid保存起来,并进行全量复制。
1.3.7 过期删除策略
面试官提问
● 常见的过期删除策略有哪些?
● Redis基于内存与CPU两方面的考虑,最终采用哪种过期策略?
常见的过期删除策略有以下3种:
(1)定时删除策略。设置key过期时间的同时创建一个定时器,在键的过期时间来临时立即删除键。定时删除及时释放内存,但浪费CPU。
(2)惰性删除策略。在访问key时顺便检查它是否过期,若过期则删除,否则返回该键值对。惰性删除策略对CPU友好,但浪费内存空间。
(3)定期删除策略。该策略是定时删除和惰性删除方案的折中,每隔一段时间执行一次删除过期key的操作,删除哪些数据库的哪些过期键由算法决定。我们通过限制执行的时长和频率来减少对CPU的影响,同时定期主动删除过期键又有效地减少了内存浪费。
Redis使用的是惰性删除和定期删除策略。
1.3.8 Redis哈希槽
面试官提问
● 解释一下哈希槽的概念。
● 为什么Redis Cluster设计成16384个槽?
Redis集群包含16384个哈希槽通过CRC16(key)%16384来计算键归属于哪个槽。集群中的每一个节点负责处理一部分哈希槽。如图1-106所示,该Redis集群拥有3个节点,哈希槽平均分配。
图1-106 Redis 16384个哈希槽
这样的设计方便添加和删除集群中的节点。例如集群中存在3个节点A、B、C,如果想添加一个新节点D,只需要将节点A,B,C负责的部分槽转移到节点D。同样,如果想从集群中删除节点A,也只需将节点A负责的槽转移到节点B和节点C,之后即可将它从集群中删除。
1.3.9 Redis Gossip协议
面试官提问
● Redis集群中各个节点是怎样交换状态信息的?
● Gossip消息类型有哪些?
Gossip协议(Gossip Protocol)是基于流行病传播方式的节点之间信息交换的协议。Redis集群中的每个节点都维护一份自己视角下的集群的状态,比如集群中各节点所负责的slots信息以及migrate状态等。为了交换不同节点的状态信息达到数据的最终一致性,集群节点之间相互发送多种消息进行通信,主要的消息命令有:
● MEET:集群中的节点向新节点发送加入集群邀请,新节点收到MEET消息后,会回复PONG命令给发送者
● PING:每个节点都会频繁地向其他节点发送PING消息,其中包含自己的状态和自己维护的集群元数据,节点之间互相通过PING交换元数据。
● PONG:PING和MEET消息的回应,包含自己的状态和其他信息,也用于信息广播和更新。
● FAIL:当一个节点判定另一个节点下线时,会向集群所有节点广播该节点挂掉的消息,其他节点收到消息后标记该节点已下线。
Redis采用Gossip协议数据同步如图1-107所示,只要每一个节点将自己的已知信息传递给其他节点,那么最终整个集群所有节点都会认知一致,类似于传染病无障碍传播最终感染整个人群。
图1-107 Redis采用Gossip协议数据同步
1.3.10 重定向moved与ask
面试官提问
● 什么是moved重定向?请说明moved重定向发生的时机。
● 什么是ask重定向?请说明ask重定向发生的时机。
● moved与ask重定向有什么区别?
1.moved重定向
Redis客户端可以向集群中的任意节点发起请求,如果key所属的槽不在当前节点,那么Redis集群就向客户端响应一个moved重定向,客户端根据moved重定向所包含的信息找到目标节点,再一次发送命令请求,如图1-108所示。
图1-108 Redis moved重定向
2.ask重定向
ask重定向发生在集群伸缩时,当客户端发送请求至源节点时,数据因为集群伸缩而迁移到了目标节点,ask异常会告诉客户端访问的键值迁移到了哪里,客户端再据此访问目标节点,如图1-109所示。
图1-109 Redis ask重定向
1.3.11 Pipeline有什么好处
面试官提问
● Pipeline有什么好处?
● 原生批命令与Pipeliner的区别是什么?
Redis客户端执行一条命令分为4个过程:发送命令、排队、执行、响应结果,如图1-110所示。
图1-110 连续执行n条命令,n次RTT
如图1-110所示,执行n条命令会产生n次RTT(Round Trip Time,往返时延),执行效率低下。
Pipeline机制允许将一组Redis命令组装后传输给Redis,Redis将这组命令的执行结果按顺序返回给客户端,整个过程只需一次RTT。
原生批命令是原子性的,但Pipeline不保证原子性,即使执行过程中某个命令出现异常,也会继续执行其他的命令。