乐趣区

关于redis:Redis-面霸篇高频问题横扫核心知识点

「码哥字节」从高频面试问题跟大家一起横扫 Redis 外围知识点,从根本上了解 Redis,不做八股文的工具人,做扭转乾坤的大神。

码哥到现在曾经写了 9 篇 Redis 连载,后盾有小伙伴也让我写一些对于面试的文章,于是“面霸”系列便出道了。

如果大家用心读完《Redis 系列》并了解,吊打面试官基本不是事。

  1. Redis 外围篇:唯快不破的机密
  2. Redis 日志篇:AOF 和 RDB 实现宕机疾速复原,数据不失落
  3. Redis 高可用篇:主从架构数据一致性同步原理
  4. Redis 实战篇:6.x 版本 Sentinel 哨兵集群搭建
  5. Redis 高可用篇:Sentinel 哨兵集群原理
  6. Redis 实战篇:6.x 版本 Cluster 集群搭建
  7. Redis 高可用篇:Cluster 集群能有限拓展么?原理是什么?
  8. Redis 实战篇:巧用 Bitmap 实现亿级海量数据统计
  9. Redis 实战篇:巧用数据结构实现亿级数据统计

Redis 为什么这么快?

很多人只晓得是 K/V NoSQl 内存数据库,单线程……这都是没有全面了解 Redis 导致无奈持续深问上来。

这个问题是根底摸底,咱们能够从 Redis 不同数据类型底层的数据结构实现、齐全基于内存、IO 多路复用网络模型、线程模型、渐进式 rehash……

到底有多快?

咱们能够先说到底有多快,依据官网数据,Redis 的 QPS 能够达到约 100000(每秒申请数),有趣味的能够参考官网的基准程序测试《How fast is Redis?》,地址:https://redis.io/topics/bench…

横轴是连接数,纵轴是 QPS。

这张图反映了一个数量级,通过量化让面试官感觉你有看过官网文档,很谨严。

基于内存实现

Redis 是基于内存的数据库,跟磁盘数据库相比,齐全吊打磁盘的速度。

不管读写操作都是在内存上实现的,咱们别离比照下内存操作与磁盘操作的差别。

磁盘调用

内存操作

内存间接由 CPU 管制,也就是 CPU 外部集成的内存控制器,所以说内存是间接与 CPU 对接,享受与 CPU 通信的最优带宽。

最初以一张图量化零碎的各种延时工夫(局部数据援用 Brendan Gregg)

高效的数据结构

学习 MySQL 的时候我晓得为了进步检索速度应用了 B+ Tree 数据结构,所以 Redis 速度快应该也跟数据结构无关。

Redis 一共有 5 种数据类型,String、List、Hash、Set、SortedSet

不同的数据类型底层应用了一种或者多种数据结构来撑持,目标就是为了谋求更快的速度。

码哥寄语:咱们能够别离阐明每种数据类型底层的数据结构长处,很多人只晓得数据类型,而说出底层数据结构就能让人眼前一亮。

SDS 简略动静字符串劣势

  1. SDS 中 len 保留这字符串的长度,O(1) 工夫复杂度查问字符串长度信息。
  2. 空间预调配:SDS 被批改后,程序不仅会为 SDS 调配所须要的必须空间,还会调配额定的未应用空间。
  3. 惰性空间开释:当对 SDS 进行缩短操作时,程序并不会回收多余的内存空间,而是应用 free 字段将这些字节数量记录下来不开释,前面如果须要 append 操作,则间接应用 free 中未应用的空间,缩小了内存的调配。

zipList 压缩列表

压缩列表是 List、hash、sorted Set 三种数据类型底层实现之一。

当一个列表只有大量数据的时候,并且每个列表项要么就是小整数值,要么就是长度比拟短的字符串,那么 Redis 就会应用压缩列表来做列表键的底层实现。

这样内存紧凑,节约内存。

quicklist

后续版本对列表数据结构进行了革新,应用 quicklist 代替了 ziplist 和 linkedlist。

quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段应用 ziplist 来紧凑存储,多个 ziplist 之间应用双向指针串接起来。

skipList 跳跃表

sorted set 类型的排序功能便是通过「跳跃列表」数据结构来实现。

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其余节点的指针,从而达到快速访问节点的目标。

跳表在链表的根底上,减少了多层级索引,通过索引地位的几个跳转,实现数据的疾速定位,如下图所示:

整数数组(intset)

当一个汇合只蕴含整数值元素,并且这个汇合的元素数量不多时,Redis 就会应用整数汇合作为汇合键的底层实现,节俭内存。

单线程模型

码哥寄语:咱们须要留神的是,Redis 的单线程指的是 Redis 的网络 IO(6.x 版本后网络 IO 应用多线程)以及键值对指令读写是由一个线程来执行的。 对于 Redis 的长久化、集群数据同步、异步删除等都是其余线程执行。

千万别说 Redis 就只有一个线程。

单线程指的是 Redis 键值对读写指令的执行是单线程。

先说官网答案,让人感觉足够谨严,而不是随声附和去背诵一些博客。

官网答案:因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最 有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就牵强附会地采纳单线程的计划了。原文地址:https://redis.io/topics/faq。

为啥不必多线程执行充分利用 CPU 呢?

在运行每个工作之前,CPU 须要晓得工作在何处加载并开始运行。也就是说,零碎须要帮忙它事后设置 CPU 寄存器和程序计数器,这称为 CPU 上下文。

切换上下文时,咱们须要实现一系列工作,这是十分耗费资源的操作。

引入多线程开发,就须要应用同步原语来爱护共享资源的并发读写,减少代码复杂度和调试难度。

单线程又什么益处?

  1. 不会因为线程创立导致的性能耗费;
  2. 防止上下文切换引起的 CPU 耗费,没有多线程切换的开销;
  3. 防止了线程之间的竞争问题,比方增加锁、开释锁、死锁等,不须要思考各种锁问题。
  4. 代码更清晰,解决逻辑简略。

I/O 多路复用模型

Redis 采纳 I/O 多路复用技术,并发解决连贯。采纳了 epoll + 本人实现的简略的事件框架。

epoll 中的读、写、敞开、连贯都转化成了事件,而后利用 epoll 的多路复用个性,绝不在 IO 上节约一点工夫。

Redis 线程不会阻塞在某一个特定的监听或已连贯套接字上,也就是说,不会阻塞在某一个特定的客户端申请解决上。正因为此,Redis 能够同时和多个客户端连贯并解决申请,从而晋升并发性。

Redis 全局 hash 字典

Redis 整体就是一个 哈希表来保留所有的键值对,无论数据类型是 5 种的任意一种。哈希表,实质就是一个数组,每个元素被叫做哈希桶,不论什么数据类型,每个桶外面的 entry 保留着理论具体值的指针。

而哈希表的工夫复杂度是 O(1),只须要计算每个键的哈希值,便晓得对应的哈希桶地位,定位桶外面的 entry 找到对应数据,这个也是 Redis 快的起因之一。

Redis 应用对象(redisObject)来示意数据库中的键值,当咱们在 Redis 中创立一个键值对时,至多创立两个对象,一个对象是用做键值对的键对象,另一个是键值对的值对象。

也就是每个 entry 保留着「键值对」的 redisObject 对象,通过 redisObject 的指针找到对应数据。

typedef struct redisObject{
    // 类型
   unsigned type:4;
   // 编码
   unsigned encoding:4;
   // 指向底层数据结构的指针
   void *ptr;
    //...
 }robj;

Hash 抵触怎么办?

Redis 通过 链式哈希 解决抵触:也就是同一个 桶外面的元素应用链表保留。然而当链表过长就会导致查找性能变差可能,所以 Redis 为了谋求快,应用了两个全局哈希表。用于 rehash 操作,减少现有的哈希桶数量,缩小哈希抵触。

开始默认应用「hash 表 1」保留键值对数据,「hash 表 2」此刻没有调配空间。当数据越来多触发 rehash 操作,则执行以下操作:

  1. 给「hash 表 2」调配更大的空间;
  2. 将「hash 表 1」的数据从新映射拷贝到「hash 表 2」中;
  3. 开释 hash 表 1 的空间。

值得注意的是,将 hash 表 1 的数据从新映射到 hash 表 2 的过程中并不是一次性的,这样会造成 Redis 阻塞,无奈提供服务。

而是采纳了 渐进式 rehash,每次解决客户端申请的时候,先从「hash 表 1」中第一个索引开始,将这个地位的 所有数据拷贝到「hash 表 2」中,就这样将 rehash 扩散到屡次申请过程中,防止耗时阻塞。

Redis 如何实现长久化?宕机后如何复原数据?

Redis 的数据长久化应用了「RDB 数据快照」的形式来实现宕机疾速复原。然而 过于频繁的执行全量数据快照,有两个重大性能开销:

  1. 频繁生成 RDB 文件写入磁盘,磁盘压力过大。会呈现上一个 RDB 还未执行完,下一个又开始生成,陷入死循环。
  2. fork 出 bgsave 子过程会阻塞主线程,主线程的内存越大,阻塞工夫越长。

所以 Redis 还设计了 AOF 写后日志记录对内存进行批改的指令记录。

面试官:什么是 RDB 内存快照?

在 Redis 执行「写」指令过程中,内存数据会始终变动。所谓的内存快照,指的就是 Redis 内存中的数据在某一刻的状态数据。

好比工夫定格在某一刻,当咱们拍照的,通过照片就能把某一刻的霎时画面齐全记录下来。

Redis 跟这个相似,就是把某一刻的数据以文件的模式拍下来,写到磁盘上。这个快照文件叫做 RDB 文件,RDB 就是 Redis DataBase 的缩写。

在做数据恢复时,间接将 RDB 文件读入内存实现复原。

面试官:在生成 RDB 期间,Redis 能够同时解决写申请么?

能够的,Redis 应用操作系统的多过程 写时复制技术 COW(Copy On Write) 来实现快照长久化,保证数据一致性。

Redis 在长久化时会调用 glibc 的函数 fork 产生一个子过程,快照长久化齐全交给子过程来解决,父过程持续解决客户端申请。

当主线程执行写指令批改数据的时候,这个数据就会复制一份正本,bgsave 子过程读取这个正本数据写到 RDB 文件。

这既保证了快照的完整性,也容许主线程同时对数据进行批改,防止了对失常业务的影响。

面试官:那 AOF 又是什么?

AOF 日志记录了自 Redis 实例创立以来所有的批改性指令序列,那么就能够通过对一个空的 Redis 实例程序执行所有的指令,也就是「重放」,来复原 Redis 以后实例的内存数据结构的状态。

Redis 提供的 AOF 配置项 appendfsync 写回策略间接决定 AOF 长久化性能的效率和安全性。

  • always:同步写回,写指令执行结束立马将 aof_buf缓冲区中的内容刷写到 AOF 文件。
  • everysec:每秒写回,写指令执行完,日志只会写到 AOF 文件缓冲区,每隔一秒就把缓冲区内容同步到磁盘。
  • no: 操作系统管制,写执行执行结束,把日志写到 AOF 文件内存缓冲区,由操作系统决定何时刷写到磁盘。

没有两败俱伤的策略,咱们须要在性能和可靠性上做一个取舍。

面试官:既然 RDB 有两个性能问题,那为何不必 AOF 即可。

AOF 写前日志,记录的是每个「写」指令操作。不会像 RDB 全量快照导致性能损耗,然而执行速度没有 RDB 快,同时日志文件过大也会造成性能问题。

所以,Redis 设计了一个杀手锏「AOF 重写机制」,Redis 提供了 bgrewriteaof指令用于对 AOF 日志进行瘦身。

其原理就是开拓一个子过程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化结束后再将操作期间产生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加结束后就立刻代替旧的 AOF 日志文件了,瘦身工作就实现了。

面试官:如何实现 数据尽可能少失落又能兼顾性能呢?

重启 Redis 时,咱们很少应用 rdb 来复原内存状态,因为会失落大量数据。咱们通常应用 AOF 日志重放,然而重放 AOF 日志性能绝对 rdb 来说要慢很多,这样在 Redis 实例很大的状况下,启动须要破费很长的工夫。

Redis 4.0 为了解决这个问题,带来了一个新的长久化选项——混合长久化 。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自长久化开始到长久化完结的这段时间产生的增量 AOF 日志,通常这部分 AOF 日志很小。

于是 在 Redis 重启的时候,能够先加载 rdb 的内容,而后再重放增量 AOF 日志就能够齐全代替之前的 AOF 全量文件重放,重启效率因而大幅失去晋升

Redis 主从架构数据同步

Redis 提供了主从模式,通过主从复制,将数据冗余一份复制到其余 Redis 服务器。

面试官:主从之间数据如何保障一致性?

为了保障正本数据的一致性,主从架构采纳了读写拆散的形式。

  • 读操作:主、从库都能够执行;
  • 写操作:主库先执行,之后将写操作同步到从库;

面试官:主从复制还有其余作用么?

  1. 故障复原:当主节点宕机,其余节点仍然能够提供服务;
  2. 负载平衡:Master 节点提供写服务,Slave 节点提供读服务,分担压力;
  3. 高可用基石:是哨兵和 cluster 施行的根底,是高可用的基石。

面试官:主从复制如何实现的?

同步分为三种状况:

  1. 第一次主从库全量复制;
  2. 主从失常运行期间的同步;
  3. 主从库间网络断开重连同步。

面试官:第一次同步怎么实现?

主从库第一次复制过程大体能够分为 3 个阶段:连贯建设阶段(即筹备阶段)、主库同步数据到从库阶段、发送同步期间新写命令到从库阶段

  1. 建设连贯:从库会和主库建设连贯,从库执行 replicaof 并发送 psync 命令并通知主库行将进行同步,主库确认回复后,主从库间就开始同步了
  2. 主库同步数据给从库:master 执行 bgsave命令生成 RDB 文件,并将文件发送给从库,同时 主库 为每一个 slave 开拓一块 replication buffer 缓冲区记录从生成 RDB 文件开始收到的所有写命令。从库保留 RDB 并清空数据库再加载 RDB 数据到内存中。
  3. 发送 RDB 之后接管到的新写命令到从库:在生成 RDB 文件之后的写操作并没有记录到刚刚的 RDB 文件中,为了保障主从库数据的一致性,所以主库会在内存中应用一个叫 replication buffer 记录 RDB 文件生成后的所有写操作。并将外面的数据发送到 slave。

面试官:主从库间的网络断了咋办?断开后要从新全量复制么?

在 Redis 2.8 之前,如果主从库在命令流传时呈现了网络闪断,那么,从库就会和主库从新进行一次全量复制,开销十分大。

从 Redis 2.8 开始,网络断了之后,主从库会采纳增量复制的形式持续同步。

增量复制:用于网络中断等状况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效

断开重连增量复制的实现神秘就是 repl_backlog_buffer 缓冲区,不论在什么时候 master 都会将写指令操作记录在 repl_backlog_buffer 中,因为内存无限,repl_backlog_buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始笼罩后面的内容

master 应用 master_repl_offset记录本人写到的地位偏移量,slave 则应用 slave_repl_offset记录曾经读取到的偏移量。

当主从断开重连后,slave 会先发送 psync 命令给 master,同时将本人的 runIDslave_repl_offset发送给 master。

master 只须要把 master_repl_offsetslave_repl_offset之间的命令同步给从库即可。

增量复制执行流程如下图:

面试官:那实现全量同步后,失常运行过程中如何同步数据呢?

当主从库实现了全量复制,它们之间就会始终保护一个网络连接,主库会通过这个连贯将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连贯的命令流传,应用长连贯的目标就是防止频繁建设连贯导致的开销。

哨兵原理连环问

面试官:能够呀,晓得这么多,你晓得 哨兵集群原理么?

哨兵是 Redis 的一种运行模式,它专一于 对 Redis 实例(主节点、从节点)运行状态的监控,并可能在主节点产生故障时通过一系列的机制实现选主及主从切换,实现故障转移,确保整个 Redis 零碎的可用性

他的架构图如下:

Redis 哨兵具备的能力有如下几个:

  • 监控:继续监控 master、slave 是否处于预期工作状态。
  • 主动切换主库:当 Master 运行故障,哨兵启动主动故障复原流程:从 slave 中抉择一台作为新 master。
  • 告诉:让 slave 执行 replicaof,与新的 master 同步;并且告诉客户端与新 master 建设连贯。

面试官:哨兵之间是如何晓得彼此的?

哨兵与 master 建设通信,利用 master 提供公布 / 订阅机制公布本人的信息,比方身高体重、是否独身、IP、端口……

master 有一个 __sentinel__:hello 的专用通道,用于哨兵之间公布和订阅音讯。这就好比是 __sentinel__:hello 微信群,哨兵利用 master 建设的微信群公布本人的音讯,同时关注其余哨兵公布的音讯

面试官:哨兵之间尽管建设连贯了,然而还须要和 slave 建设连贯,不然没法监控他们呀,如何晓得 slave 并监控他们的?

要害还是利用 master 来实现,哨兵向 master 发送 INFO 命令,master 掌门天然是晓得本人门下所有的 salve 小弟的。所以 master 接管到命令后,便将 slave 列表通知哨兵。

哨兵依据 master 响应的 slave 名单信息与每一个 salve 建设连贯,并且依据这个连贯继续监控哨兵。

Cluster 集群连环炮

面试官:除了哨兵以外,还有其余的高可用伎俩么?

有 Cluster 集群实现高可用,哨兵集群监控的 Redis 集群是主从架构,无奈很想拓展。应用 Redis Cluster 集群,次要解决了大数据量存储导致的各种慢问题,同时也便于横向拓展。

在面向百万、千万级别的用户规模时,横向扩大的 Redis 切片集群会是一个十分好的抉择。

面试官:什么是 Cluster 集群?

Redis 集群是一种分布式数据库计划,集群通过分片(sharding)来进行数据管理(「分治思维」的一种实际),并提供复制和故障转移性能。

将数据划分为 16384 的 slots,每个节点负责一部分槽位。槽位的信息存储于每个节点中。

它是去中心化的,如图所示,该集群有三个 Redis 节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。

三个节点相互连接组成一个对等的集群,它们之间通过 Gossip协定互相交互集群信息,最初每个节点都保留着其余节点的 slots 分配情况。

面试官:哈希槽又是如何映射到 Redis 实例上呢?

  1. 依据键值对的 key,应用 CRC16 算法,计算出一个 16 bit 的值;
  2. 将 16 bit 的值对 16384 执行取模,失去 0 ~ 16383 的数示意 key 对应的哈希槽。
  3. 依据该槽信息定位到对应的实例。

键值对数据、哈希槽、Redis 实例之间的映射关系如下:

面试官:Cluster 如何实现故障转移?

Redis 集群节点采纳 Gossip 协定来播送本人的状态以及本人对整个集群认知的扭转。比方一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群播送,其它节点也就能够收到这点失联信息。

如果一个节点收到了某个节点失联的数量 (PFail Count) 曾经达到了集群的大多数,就能够标记该节点为确定下线状态 (Fail),而后向整个集群播送,强制其它节点也接管该节点曾经下线的事实,并立刻对该失联节点进行主从切换。

面试官:客户端又怎么确定拜访的数据到底散布在哪个实例上呢?

Redis 实例会将本人的哈希槽信息通过 Gossip 协定发送给集群中其余的实例,实现了哈希槽调配信息的扩散。

这样,集群中的每个实例都有所有哈希槽与实例之间的映射关系信息。

当客户端连贯任何一个实例,实例就将哈希槽与实例的映射关系响应给客户端,客户端就会将哈希槽与实例映射信息缓存在本地。

当客户端申请时,会计算出键所对应的哈希槽,再通过本地缓存的哈希槽实例映射信息定位到数据所在实例上,再将申请发送给对应的实例。

面试官:什么是 Redis 重定向机制?

哈希槽与实例之间的映射关系因为新增实例或者负载平衡重新分配导致扭转了,客户端将申请发送到实例上,这个实例没有相应的数据,该 Redis 实例会通知客户端将申请发送到其余的实例上

Redis 通过 MOVED 谬误和 ASK 谬误通知客户端。

MOVED

MOVED 谬误(负载平衡,数据曾经迁徙到其余实例上):当客户端将一个键值对操作申请发送给某个实例,而这个键所在的槽并非由本人负责的时候,该实例会返回一个 MOVED 谬误指引转向正在负责该槽的节点。

同时,客户端还会更新本地缓存,将该 slot 与 Redis 实例对应关系更新正确

ASK

如果某个 slot 的数据比拟多,局部迁徙到新实例,还有一部分没有迁徙。

如果申请的 key 在以后节点找到就间接执行命令,否则时候就须要 ASK 谬误响应了。

槽局部迁徙未实现的状况下,如果须要拜访的 key 所在 Slot 正在从 实例 1 迁徙到 实例 2(如果 key 曾经不在实例 1),实例 1 会返回客户端一条 ASK 报错信息:客户端申请的 key 所在的哈希槽正在迁徙到实例 2 上,你先给实例 2 发送一个 ASKING 命令,接着发发送操作命令

比方客户端申请定位到 key =「公众号: 码哥字节」的槽 16330 在实例 172.17.18.1 上,节点 1 如果找失去就间接执行命令,否则响应 ASK 错误信息,并指引客户端转向正在迁徙的指标节点 172.17.18.2。

留神:ASK 谬误指令并不会更新客户端缓存的哈希槽调配信息

未完待续

本篇次要将 Redis 核心内容过了一遍,波及到数据结构、内存模型、IO 模型、长久化 RDB 和 AOF、主从复制原理、哨兵原理、cluster 原理。

「面霸」系列会分为几篇,别离从外围原理、高可用、实战、如何避坑等方面全方位拿下 Redis。

大家感觉有所帮忙心愿能够动动手指导赞、分享、珍藏、留言呀,也能够加「码哥」微信「MageByte1024」进入专属读者群一起探讨更多面试遇到的问题,码哥知无不答。

退出移动版