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

「码哥字节」从高频面试问题跟你们一块儿横扫 Redis 核心知识点,从根本上理解 Redis ,不作八股文的工具人,作扭转乾坤的大神。c++

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

若是你们用心读完《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/benchmarks数组

基准测试

横轴是链接数,纵轴是 QPS。缓存

这张图反映了一个数量级,经过量化让面试官以为你有看过官方文档,很严谨。安全

基于内存实现

Redis 是基于内存的数据库,跟磁盘数据库相比,彻底吊打磁盘的速度。服务器

不论读写操做都是在内存上完成的,咱们分别对比下内存操做与磁盘操做的差别。微信

磁盘调用

内存操做

内存直接由 CPU 控制,也就是 CPU 内部集成的内存控制器,因此说内存是直接与 CPU 对接,享受与 CPU 通讯的最优带宽。

最后以一张图量化系统的各类延时时间(部分数据引用 Brendan Gregg)

高效的数据结构

学习 MySQL 的时候我知道为了提升检索速度使用了 B+ Tree 数据结构,因此 Redis 速度快应该也跟数据结构有关。

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

不一样的数据类型底层使用了一种或者多种数据结构来支撑,目的就是为了追求更快的速度。

码哥寄语:咱们能够分别说明每种数据类型底层的数据结构优势,不少人只知道数据类型,而说出底层数据结构就能让人眼前一亮。

SDS 简单动态字符串优点

C 语言字符串与 SDS

  1. SDS 中 len 保存这字符串的长度,O(1) 时间复杂度查询字符串长度信息。
  2. 空间预分配:SDS 被修改后,程序不只会为 SDS 分配所须要的必须空间,还会分配额外的未使用空间。
  3. 惰性空间释放:当对 SDS 进行缩短操做时,程序并不会回收多余的内存空间,而是使用 free 字段将这些字节数量记录下来不释放,后面若是须要 append 操做,则直接使用 free 中未使用的空间,减小了内存的分配。

zipList 压缩列表

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

当一个列表只有少许数据的时候,而且每一个列表项要么就是小整数值,要么就是长度比较短的字符串,那么 Redis 就会使用压缩列表来作列表键的底层实现。

ziplist

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

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 上浪费一点时间。

高性能 IO 多路复用

Redis 线程不会阻塞在某一个特定的监听或已链接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正由于此,Redis 能够同时和多个客户端链接并处理请求,从而提高并发性。

Redis 全局 hash 字典

Redis 总体就是一个 哈希表来保存全部的键值对,不管数据类型是 5 种的任意一种。哈希表,本质就是一个数组,每一个元素被叫作哈希桶,无论什么数据类型,每一个桶里面的 entry 保存着实际具体值的指针。

Redis 全局哈希表

而哈希表的时间复杂度是 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 文件读入内存完成恢复。

面试官:在生成 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 日志文件了,瘦身工做就完成了。

AOF重写机制(纠错:3条变一条)

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

重启 Redis 时,咱们不多使用 rdb 来恢复内存状态,由于会丢失大量数据。咱们一般使用 AOF 日志重放,可是重放 AOF 日志性能相对 rdb 来讲要慢不少,这样在 Redis 实例很大的状况下,启动须要花费很长的时间。

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

因而在 Redis 重启的时候,能够先加载 rdb 的内容,而后再重放增量 AOF 日志就能够彻底替代以前的 AOF 全量文件重放,重启效率所以大幅获得提高

Redis 主从架构数据同步

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

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

为了保证副本数据的一致性,主从架构采用了读写分离的方式。

  • 读操做:主、从库均可以执行;
  • 写操做:主库先执行,以后将写操做同步到从库;

Redis 读写分离

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

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

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

同步分为三种状况:

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

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

主从库第一次复制过程大致能够分为 3 个阶段:链接创建阶段(即准备阶段)、主库同步数据到从库阶段、发送同步期间新写命令到从库阶段

Redis全量同步

  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记录已经读取到的偏移量。

repl_backlog_buffer

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

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

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

Redis增量复制

面试官:那完成全量同步后,正常运行过程当中如何同步数据呢?

当主从库完成了全量复制,它们之间就会一直维护一个网络链接,主库会经过这个链接将后续陆续收到的命令操做再同步给从库,这个过程也称为基于长链接的命令传播,使用长链接的目的就是避免频繁创建链接致使的开销。

哨兵原理连环问

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

哨兵是 Redis 的一种运行模式,它专一于对 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 创建链接,而且根据这个链接持续监控哨兵。

INFO命令获取slave信息

Cluster 集群连环炮

面试官:除了哨兵之外,还有其余的高可用手段么?

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

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

面试官:什么是 Cluster 集群?

Redis 集群是一种分布式数据库方案,集群经过分片(sharding)来进行数据管理(「分治思想」的一种实践),并提供复制和故障转移功能。

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

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

Redis 集群架构

三个节点相互链接组成一个对等的集群,它们之间经过 Gossip协议相互交互集群信息,最后每一个节点都保存着其余节点的 slots 分配状况。

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

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

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

数据、Slot与实例的映射

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

Redis 集群节点采用 Gossip 协议来广播本身的状态以及本身对整个集群认知的改变。好比一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就能够收到这点失联信息。

若是一个节点收到了某个节点失联的数量 (PFail Count) 已经达到了集群的大多数,就能够标记该节点为肯定下线状态 (Fail),而后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并当即对该失联节点进行主从切换。

面试官:客户端又怎么肯定访问的数据到底分布在哪一个实例上呢?

Redis 实例会将本身的哈希槽信息经过 Gossip 协议发送给集群中其余的实例,实现了哈希槽分配信息的扩散。

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

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

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

Redis 客户端定位数据所在节点

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

哈希槽与实例之间的映射关系因为新增实例或者负载均衡从新分配致使改变了,客户端将请求发送到实例上,这个实例没有相应的数据,该 Redis 实例会告诉客户端将请求发送到其余的实例上

Redis 经过 MOVED 错误和 ASK 错误告诉客户端。

MOVED

MOVED 错误(负载均衡,数据已经迁移到其余实例上):当客户端将一个键值对操做请求发送给某个实例,而这个键所在的槽并不是由本身负责的时候,该实例会返回一个 MOVED 错误指引转向正在负责该槽的节点。

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

MOVED 指令

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 错误

注意:ASK 错误指令并不会更新客户端缓存的哈希槽分配信息

未完待续

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

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

你们以为有所帮助但愿能够动动手指点赞、分享、收藏、留言呀,也能够加「码哥」微信「MageByte1024」进入专属读者群一块儿探讨更多面试遇到的问题,码哥知无不答。

相关文章
相关标签/搜索