[Day22] Redis 八股文 [1/2]
Redis 八股文 [1/2]
目录
[toc]
数据类型
介绍一下 Redis 中常见的数据类型;每种数据类型有哪些使用场景
Redis 中常用的数据类型有五个,String、List、Set、ZSet、Hash; 还有高级的数据类型:bitmap、hyperLogLog、streams、GEO
String:使用频率最高的类型,缓存,计数器 (INCR),分布式锁 Set:去重,并查集操作,共同关注 ZSet:排行榜,延迟队列 Hash:保存对象 hash 可以看作 ` Map<String, Map<String, String>>
GEO:附近的人,附近骑手司机 hyperLogLog:UV 统计,UV 指的是独立访问量 stream:消息流
你了解 SDS 吗?介绍一下为什么要定义 SDS?
SDS 是 Redis 定义的结构,是为了解决 C 底层对字符串操作的局限性; 在原生的 C 中是通过字符数组的方式存储字符串的;所以就有一下的局限性,并且是通过‘\0’:
- 获取字符串长度的时间复杂度是 O (n);
- 字符串如果修改增长,那么就需要重新分配空间并且进行一次 O (n) 的字符串复制;因为如果在之前的地址上进行增长那么会发生内存踩踏问题
- 因为是通过'\0'标志字符串的结束,那么如果在将一些二进制文件进行存储的时候可能会将数据中的‘\0’认作结束
所以 SDS 的就是为了解决上述问题对 C 原生字符串出的一个补丁,不直接舍弃 C 原生字符串的方式是为了在底层利用 C 的高效和复用其他 C 字符串操作库; SDS 是定义的一个对象,说对象不准确因为 C 是面向过程的应该说是一个结构体,结构体中定义了三个字段,len 字符串的长度,alloc 字段表示当前字符串的空间总容量,和一个字符数组 buf 用于和 C 一样存储字符串;并且规定了如果字符串长度小于 1 M 那么空间就是字符串的两倍,如果字符串大于 1 M 那么就预分配 1 M 的空间;
- SDS 解决 C 原生字符串获取长度过慢问题,直接读取 len 字段的值 O (n) 优化到 O (1);java 中的 String、Map、List 底层都是通过相同的思想加速获取长度的;
- 提高了 C 原生字符串长度变化时候的空间分配效率,采用的是空间换时间的方法,提前准备冗余空间;
- 解决了二进制安全问题,因为在 SDS 中不依赖字符串结束标志位判断字符换结束,而是通过长度字段
讲讲 Redis 中各个数据类型的底层
在 Redis 7 之前 (之后)
- String:SDS(SDS)
- List:quickList+zipList(quickList + packList)
- Set:intsSet+hashTable(intset+hashTable)
- ZSet:skipList+zipList(skipList+listPack)
- Hash:hashTable+zipList (hashTable+listPack)
Hash 的底层结构是怎么样的?何时会从 ziplist 转成 hashtable?
Redis 6 之前 Hash 的实现是通过 HashTable+zipList 实现的; zipList 有两个阈值,分别是当前 hash 中 key 的最多个数和当前 hash 中 value 的字节最大上限; 当两个阈值都没达到时 hash 会采用 ziplist 进行存储,任意一个阈值被打破了那么就会升级为 Hashtable;默认最大个数是 512 默认最大 value 长度是 64;只能升级不会降级
介绍一下 ZipList 的特点;为什么会用 listPack 对他进行替换?
zipList 的核心思想就是用时间换空间,思想和 JVM 垃圾回收时候的标记-整理算法,虽然耗时间,但是空间利用率高;zipList 就是采用这个思想,在逻辑上 zipList 上双向链表,但是他并不保存前后指针,而是通过保存上个节点的长度,通过地址偏移来定位具体节点的,是从后往前的遍历;因为 zipList 在内存上是一段连续的空间,所以他的优点和缺点就很明显了; 优点是空间利用率很高,在少量数据存储的情况在效率很高;但是缺点就是每次寻址都要经过偏移计算,时间和 CPU 开销大,并且数据量大的情况在效率会急剧下降;还有一个很严重的问题,就是每个 zipList 中的节点都保存着前面节点的大小,也就意味着如果其中一个节点发生更新那么就会引发后面所有节点进行更新,过程中可能会触发某些节点的膨胀,就会需要重新分配空间的 问题;
listpack 主要是在 ziplist 的基础上做了升级,在具备 ziplist 优点的同时弥补了一些缺点;listpack 不再保存前面一个节点的长度,而是保存当前节点的尺寸,这样的好处就是当前节点发生了尺寸变化也不会影响到其他节点;同时 listpack 还在便面和布局方面做了优化;其他方面不论是实现思想还是升级到 hashtable 的策略都和 ziplist 相同
List 的底层是链表还是数组?为什么?
List 的底层是一个 quickList 是一个双向链表,而这个双向链表的每个节点都是 zipList; quickList 是双向链表,zipList 也是链表,但是 zipList 在空间是连续紧凑的,所以可以认为 List 是结合了数组和链表各种的优点所诞生的,他兼具了数组在空间上的紧凑性又具备了链表在节点操作方面的灵活性; 在 Reids 7 开始就将 zipList 替换成了 listpack,来解决 zipList 连锁更新还有内存布局和结构上的局限性;
介绍一下 Set 的底层结构
Set 底层的实现是 intSet 和 HashTable 实现的; 当插入的元素都是整型并且元素的个数没有超过阈值默认 512 的情况在用的就是 intset;当这两个条件有一个不满足那么就会升级为 HashTbale;intset 底层采用的是有序排列的方式,通过二分查找定位元素所以时间复杂度是 O(logn)
介绍一下 ZSet 的底层结构
ZSet 底层是 zipList 和 skipList 实现的;当元素个数小于阈值默认 128,并且所有元素的值都小于最大值阈值默认 64 的时候默认采用的是 ziplist 实现的;如果超过的情况就转为跳表
介绍一下跳表;
跳表增删改查的时间复杂度是 O(logN); 可以理解为他是在普通单向链表的基础上为每个节点加上了索引,加速的数据的定位,思想就是空间换时间;具体实现是将在单链表的基础上为每个节点进行升维操作,每次通过随机决定每个节点应该有几层高,然后一层索引都指向通一层的下一个索引;这样在每次进行元素查找的时候,比如现在 a 值的第 n 层索引,我们要查找 100,我们看 a 在 n 层的下一个值 b 是否大于 100,如果大于 100说明 100 在[a, b)这个左闭右开的区间内,我们就将层数-1;如果大于 b 说明[a, b]区间内不存在 100 直接跳到 b 的第 n 层;以此类推,直到到达第 1 层判断当前值是否为我们要查找的 100;
为什么 Zset 使用跳表实现不用红黑树?
这个问题当时我在学习 Redis 的时候也是考过,也查了很多的文章和博客,说法大致可以分为以下几点,跳表的空间开销更低,跳表的并发效率更好,跳表的查询效率更高;不论是从理论性能还有网上的很多实际性能比对都显示两者性能没什么区别,虽然跳表的并发性能更好,因为每次需要锁定的节点会比红黑树锁定一个节点和下面的所有节点性能更高,但是 Redis 是单线程的不存在并发问题;最后我认为 Redis 选择跳表而不是红黑树的原因应该就是实现起来更简单,因为 Redis 的作者之前反复过说“红黑树和跳表性能相当,但是跳表代码层面实现更简单”
什么是 Redis 的渐进式 rehash
Redis 中 hashTable 和 java 中 hashMap 一样都是通过链地址法来解决 hash 冲突的,那么就一定要面临一个问题,就是哈希表在扩容后所有元素映射位置重新计算的问题也就是 rehash;在 java 中是通过将链表分成高位链表和低位链表低位下标不变,高位移动扩容长度的下标,还会通过开启多线程来加速这个 rehash 的过程,但是 redis 是单线程应用如果出现 rehash 那么就会出现长时间的阻塞所以就有了渐进式 rehash;总体思路优点类似于 JVM 垃圾回收中的标记复制算法,就是每个 redis 中的哈希表都有一个冗余表;当发生扩容时就会通过一个 rehashIndex 来标记当前 rehash 的下标,每次进行 CRUD 操作都将对应下标的原表数据 rehash 到冗余表中然后 index+1;当原表中的所有桶数据都完成了 rehash 之后将冗余表地址复制给原表,容易表置为空,然后相同的流程等待下一次扩容;
持久化
介绍一下 Redis 的持久化策略
RDB
RDB, Redis database;是通过自动触发或者手动触发对我们 Redis 中的数据做全量的数据快照,然后通过压缩转换为二进制文件 dump. rdb; 手动触发的话可以通过 save 和 bgsave;save 会阻塞线程进行快照,bgsave 时主线程会 fork 一个子线程进行同步地备份;自动备份可以通过命令 save m n 表示在 m 时间内发生了 n 次修改操作的话就会触发快照; RDB 这种方式的优势是:生成的 rdb 文件大小经过压缩小于实际 redis 内存数据;RDB 备份的数据量大是全量备份;RDB 的效率更高因为利用了多线程进行持久化; RDB 的劣势也很明显:RDB 进行快照的频率定成多少合适呢,不论定多少,在最后一次快照后的数据依然会丢失;还有就是主线程 fork 子线程的这个动作也会进行短暂的阻塞,当集群数量大的时候会让这个阻塞时间变长;
AOF
AOF 的实现思想类似于 Mysql 中 bin log 的 statment 格式;就是记录每一条修改操作命令,让 redis 宕机后会通过记录所有命令的表从上往下依次执行从而来恢复 redis;AOF 默认是关闭的,我们开启后还需要指定将指令写到磁盘中的频率,有每次修改指令都写入磁盘这样的方法数据安全性最高但是磁盘压力会大;还有比较推荐使用的每秒写入,这样的方式是性能和安全比较平衡的方式; AOF 的优势就是数据完整性强,缺点就是文件的体积会大于 RDB 生成的快照;还有个缺点就是 AOF 属于逻辑日志,无法保证同一条指令多次执行的结果一直,比如涉及到随机数生产,同一条指令虽然被备份了但是无法保证和当时的执行结果一致,比如 randomKey,randomNum;
混合持久化
在 redis4 之后推出了混合持久化模式就是结合 RDB+AOF;RDB 做全量数据的快照,AOF 做增量指令的记录;简单理解就是将 RDB 文件的二进制数据写入到 AOF 文件的开头,然后新增的没被 RDB 所记录的指令就通过 AOF 的方式进行追加;
事务
Redis 的事务机制是怎样的?Redis 的事务和 MySQL 的事务区别?
Redis 中的事务是将多条指令组成一个原子命令,会将一个事务中的指令放入一个队列中,然后 Redis 会顺序的执行这些,过程中不会被其他命令插入和打断;但是不同于传统关系型数据库中的事务,Reids 的事务不能保证所有的事务同时成功或者失败,也就是说一个原子事务中的多个命令可能中间有些执行失败或者报错,那么 redis 也并不会进行和 Mysql 一样的回滚操作;并且 Redis 中没有事务隔离级别的概念,因为 Redis 是单线程的不用考虑事务并发操作也就不要考虑隔离性和脏读幻读问题;
开启事务 multi 提交事务 exec 放弃提交 discard
Redis 如何实现乐观锁的
Redis 中通过 watch 来监控一个 key;如果我们在事务中操作一个被监控 key 的时候会通过乐观锁的思想查看当然操作的 key 是否在期间被人修改过,如果被修改过就放弃起执行,结合我们上层的重试机制就能实现 CAS 的原子修改;监控完后可以通过 unwatch 来取消监控;
为什么 Redis 不支持回滚?
这要从 Redis 的设计思想上解释了,因为在 Redis 官方中说明,Redis 完全有能力实现和 Mysql 一样的回滚,但是这样一来 Redis 的整体复杂度会变高,影响整体的效率;Redis 的设计目标一定是高效,并且 Redis 中一般作为缓存不会涉及特别复杂的事务回滚需求,所以 Redis 中的事务不会回滚,如果在使用过程中需要回滚机制我们可以通过 Lua 脚本、watch 监控等其他机制进行弥补;
管道
介绍一下 Redis 中的管道技术
管道技术是为了减少多次客户端和 Redis 请求过程的 RTT 时间,RTT 是指条指令从客户端发送到服务端,服务端阻塞处理完成将结果返回到客户端过程所耗时间;如果频繁地发送命令,那么 RTT 时间就会很长;管道技术就是为了解决这个问题诞生的,他的思想和 Redis 原生的批处理命令 mset 一样,将多条命令进行打包封装通过一次网络 IO 传输到 Redis 服务器,Redis 处理完成之后也只用通过一个打包将结果返回;提高了我们的信道复用率提高整体性能;
事务和 Pipeline 的区别?
管道技术是在传输层面,将多条命令打包传输来减少网络 IO 开销;而事务是在 Redis 层面将多个命令放入同一个队列中,然后从队列中依次执行命令让这些命令在执行过程中不会被其他命令打断和抢占;管道提高的是传输效率,事务保证的是多条命令的整体原子性;
发布订阅
介绍一下 Redis 中的发布订阅
Redis 的发布订阅是基于 5.0 推出的 Stream 流类型实现的;他的实现流程就是订阅频道、向指定频道推送消息、取消订阅;一个客户端可以订阅一个或多个频道,消息的推送者向这些频道中推送消息,所有的该频道订阅者都能收到;
Redis 的发布订阅和主流 MQ 中间件的区别
Redis 的发布订阅只能针对一些对数据可靠性要求不严格的场景,因为 redis 的发布订阅没有接收确认机制也没有持久化机制,但是在某些对数据要求不是很高的场景下可以高效实现消息组播;
复制
介绍一下 Redis 中的主从复制
Redis 的主从复制是 Redis 集群最简单的架构,是将一个主 Redis 节点的数据备份给一个或者多个从节点;这样就能实现读写分离,主库进行数据修改操作,从库处理数据查询操作,提高了整体的性能;还可以实现备份数据容灾恢复,还支持水平扩容实现高并发; 主从复制的集群架构适用于读多写少的场景;主从架构的最大缺点就是所有的从节点依赖主节点的健壮性,如果主节点发生故障,那么还没备份到从节点的数据将丢失,并且没办法自主实现故障转移;并且主从架构在节点增多的情况下性能会变低,如果依赖树每一层的节点很多那么上层节点的压力就会很大,如果树的高度很高那么同步的延迟和抖动就会很大;
介绍一下主从复制的流程
- 从节点启动时会向主节点发送同步请求,主服务器会验证从服务器的认证信息;
- 通过后如果是第一次同步,那么主节点会在后台触发 RDB 生成快照,同时会将新的数据修改命令进行缓存,然后打包同步给从节点;也就是新加入从节点时会触发全量更新
- 在同步完成之后,从节点就周期性的发送心跳检测监听主节点的健康状况
- 如果过程中主节点有了新的修改操作,那么从节点会通过增量更新的方式同步数据
- 最后就是断点续传,从节点恢复连接后会通过主从的同步标志位,判断哪些数据已经同步过了哪些数据需要同步,从而实现断点续传的效果;
哨兵
介绍一下哨兵架构
哨兵架构是在主从复制的基础上实现了高可用;他在原有的主从节点之上定义了多个哨兵节点一半为奇数这样方便投票选举,这些哨兵节点的职责是监控集群中所有节点的状态;如果一个哨兵发现主节点断开连接了 (可以配置心跳检测的等待时间超过时间没有接收到回复就任务断开连接了)那么就会将它标记为主观下线,因为某一时候可能由于网络波动等原因导致一个哨兵没有检测到主节点的活跃,当多个哨兵都将同一个主节点标记为主观下线后这个值达到 quorum 法定票数后,我们就认为这个主节点客观下线了,这个时候就会触发哨兵的故障转移;,哨兵之间会通过 Raft 算法选举出领导者 leader,哨兵们中的领导者 leader 会在所有健康的从节点中推选一个新节点作为主节点,并且通知其他从节点切换新的主节点连接;与此同时会向客户通过发布订阅的方式告知主节点的变化; 优点:在主从复制的基础上实现了故障转移;转移过程对客户端而言是透明的不用主动干涉 缺点:主节点切换时间长一般 10 秒以上;写入操作的压力依然在主节点无法拓展;
哨兵如何选择新主节点?
首先所有的哨兵都会通过心跳检测的方式检测各个节点的状态,如果主节点的应答时间超过了设定的最大等大阈值依然没有应答某个哨兵节点,那么这个哨兵节点就会把他标记为主观下线,因为主节点可能是健康的但是因为网络波动导致这次心跳检测延迟,但是如果多个哨兵都将主节点标记为了主观下线并且数量达到了法定投票 quorum,那么哨兵集群就会认为主节点客观下线了,接下来就需要进行新的主节点选举; 首先哨兵之间会通过 Raft 算法选举出一个 leader 哨兵,后续的通知操作都通过 leader 哨兵发出;Raft 算法简单来说就是每个哨兵都持有一票,每个哨兵都会通过广播的方式进行拉票,比如 A 向 B 发送请求,如果 B 此时手上持有票那么就会投给 A ,同时 A 尝试向其他节点发送同样的请求,其他节点也同时发送请求;最后得票最高的哨兵就是 leader; leader 会从目前健康的从节点中选举一个作为新的主节点,会首先根据每个从节点配置文件中的优先级进行比较,谁小说明谁的优先级高进行选举,如果多个优先级一样那么就根据这些从节点的复制偏移指针来比较,因为复制偏移指针标志的是从节点复制主节点的进度,选择一个进度最快的可以尽可能的减少数据的丢失,如果偏移指针还相同就会通过节点的 id ascii 码选择最小的; 选定新的主节点后,leader 节点会让新节点执行 salve of on one,并且通知其他节点切换新的主节点;
介绍下 Redis 集群的脑裂问题?
脑裂问题是指在分布式架构中一般有一个中心化的节点来进行整体系统的调度;比如配置中心、分布式事务、分布式任务都是通过中心化的思想,而脑裂是指在分布式中的中心节点因为网络分区或则选举缺陷出现了多个中心节点,让原本统一的分布式系统分裂成了两个独立的系统,而每个系统中的节点都无法感知都认为当前的系统状态是正常的,这就是脑裂;
Cluster 集群
介绍一下中的槽位和分区算法
普通的哈希算法和节点的个数强相关,如果节点发生变动那么所有的数据都需要进行 rehash;在节点扩缩容和故障转换的时候效率很低; 一致性哈希,解决了扩缩容问题并且负载均衡效果也很好,但是在节点较少的情况下可能出现数据倾斜的问题; Cluster 集群采用的是 hash slot 哈希槽哈希算法的方式实现的;具体来说,现在客户端的请求不会通过 hash 映射到某台 redis 节点,而是在客户端和 redis 节点中加入了一层 hash 槽,hash 槽分为 16384 个 2^14 ;key 会通过 CRC 16 算法将 key 映射到槽上,这样一来映射就不会应该槽后面 Redis 节点的变动而改变; CRC 16 是循环冗余检测,在网络通讯中很常用,对接收到的数据做完整性校验;具体实现是通过对数据按位进行多项式除法运算最后得到一个 16 位的校验码;
为什么哈希槽的长度是 16384?
主要是处于一下亮点考虑:
- 如果设为 2 的 16 次方那么心跳检测包的体积会变大
- 2 的 14 次方 16384 个槽已经足够用了;redis 官网明确说明了不建议我们的几点超过 1000; 所以将大小设定为 16384 是性能和空间上最平衡的选择;
介绍一下 Cluster 中的数据分片
#java##面试#Redis 的 Cluster 集群架构通过哈希槽的方式对数据进行分片,请求不再直接映射到具体的 redis 节点,而是映射到对应的哈希槽上,而每个 redis 节点都负责处理某一片槽的请求;这样设计的好处是在 redis 节点发生扩缩容或则故障转移的时候可以做到十分高效快捷,并且不会大面积影响其他节点的效率;