妈妈不再担忧我面试被Redis问得脸都绿了

长文前排提醒,收藏向前排提醒,素质三连 (转发 + 在看 + 留言) 前排提醒!html

前言

Redis 做为一个开源的,高级的键值存储和一个适用的解决方案,已经愈来愈在构建 「高性能」「可扩展」 的 Web 应用上发挥着举足轻重的做用。前端

当今互联网技术架构中 Redis 已然成为了应用得最普遍的中间件之一,它也是中高级后端工程 技术面试 中面试官最喜欢问的工程技能之一,不只仅要求着咱们对 基本的使用 进行掌握,更要深层次地理解 Redis 内部实现 的细节原理。node

熟练掌握 Redis,甚至能够绝不夸张地说已经半只脚踏入心仪的公司了。下面咱们一块儿来盘点回顾一下 Redis 的面试经典问题,就不要再被面试官问得 脸都绿了 呀!git

  • Ps: 我把 重要的知识点 都作成了 图片,但愿各位 "用餐愉快"(不错记得付餐费.. 点个赞留个言..)

1、基础篇

什么是 Redis ?

先解释 Redis 基本概念

Redis (Remote Dictionary Server) 是一个使用 C 语言 编写的,开源的 (BSD许可) 高性能 非关系型 (NoSQL)键值对数据库程序员

简单提一下 Redis 数据结构

Redis 能够存储 不一样类型数据结构值 之间的映射关系。键的类型只能是字符串,而值除了支持最 基础的五种数据类型 外,还支持一些 高级数据类型github

必定要说出一些高级数据结构 (固然你本身也要了解.. 下面会说到的别担忧),这样面试官的眼睛才会亮。web

Redis 小总结

与传统数据库不一样的是 Redis 的数据是 存在内存 中的,因此 读写速度 很是 ,所以 Redis 被普遍应用于 缓存 方向,每秒能够处理超过 10 万次读写操做,是已知性能最快的 Key-Value 数据库。另外,Redis 也常常用来作 分布式锁面试

除此以外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。redis

Redis 优缺点

优势

  • 读写性能优异, Redis能读的速度是 110000 次/s,写的速度是 81000 次/s。
  • 支持数据持久化,支持 AOF 和 RDB 两种持久化方式。
  • 支持事务,Redis 的全部操做都是原子性的,同时 Redis 还支持对几个操做合并后的原子性执行。
  • 数据结构丰富,除了支持 string 类型的 value 外还支持 hash、set、zset、list 等数据结构。
  • 支持主从复制,主机会自动将数据同步到从机,能够进行读写分离。

缺点

  • 数据库 容量受到物理内存的限制,不能用做海量数据的高性能读写,所以 Redis 适合的场景主要局限在较小数据量的高性能操做和运算上。
  • Redis 不具有自动容错和恢复功能,主机从机的宕机都会致使前端部分读写请求失败,须要等待机器重启或者手动切换前端的 IP 才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换 IP 后还会引入数据不一致的问题,下降了 系统的可用性
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源形成了很大的浪费。

为何要用缓存?为何使用 Redis?

提一下如今 Web 应用的现状

在平常的 Web 应用对数据库的访问中,读操做的次数远超写操做,比例大概在 1:93:7,因此须要读的可能性是比写的可能大得多的。当咱们使用 SQL 语句去数据库进行读写操做时,数据库就会 去磁盘把对应的数据索引取回来,这是一个相对较慢的过程。算法

使用 Redis or 使用缓存带来的优点

若是咱们把数据放在 Redis 中,也就是直接放在内存之中,让服务端直接去读取内存中的数据,那么这样 速度 明显就会快上很多 (高性能),而且会 极大减少数据库的压力 (特别是在高并发状况下)

记得是 两个角度 啊.. 高性能高并发..

也要提一下使用缓存的考虑

可是使用内存进行数据存储开销也是比较大的,限于成本 的缘由,通常咱们只是使用 Redis 存储一些 经常使用和主要的数据,好比用户登陆的信息等。

通常而言在使用 Redis 进行存储的时候,咱们须要从如下几个方面来考虑:

  • 业务数据经常使用吗?命中率如何? 若是命中率很低,就没有必要写入缓存;
  • 该业务数据是读操做多,仍是写操做多? 若是写操做多,频繁须要写入数据库,也没有必要使用缓存;
  • 业务数据大小如何? 若是要存储几百兆字节的文件,会给缓存带来很大的压力,这样也没有必要;

在考虑了这些问题以后,若是以为有必要使用缓存,那么就使用它!

使用缓存会出现什么问题?

通常来讲有以下几个问题,回答思路遵守 是什么为何怎么解决

  1. 缓存雪崩问题;
  2. 缓存穿透问题;
  3. 缓存和数据库双写一致性问题;

缓存雪崩问题

另外对于 "Redis 挂掉了,请求所有走数据库" 这样的状况,咱们还能够有以下的思路:

  • 事发前:实现 Redis 的高可用(主从架构 + Sentinel 或者 Redis Cluster),尽可能避免 Redis 挂掉这种状况发生。
  • 事发中:万一 Redis 真的挂了,咱们能够设置本地缓存(ehcache) + 限流(hystrix),尽可能避免咱们的数据库被干掉(起码能保证咱们的服务仍是能正常工做的)
  • 事发后:Redis 持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。

缓存穿透问题

缓存与数据库双写一致问题

双写一致性上图仍是稍微粗糙了些,你还须要知道两种方案 (先操做数据库和先操做缓存) 分别都有什么优点和对应的问题,这里不做赘述,能够参考一下下方的文章,写得很是详细。

Redis 为何早期版本选择单线程?

官方解释

由于 Redis 是基于内存的操做,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有多是 机器内存的大小 或者 网络带宽。既然单线程容易实现,并且 CPU 不会成为瓶颈,那就瓜熟蒂落地采用单线程的方案了。

简单总结一下

  1. 使用单线程模型能带来更好的 可维护性,方便开发和调试;
  2. 使用单线程模型也能 并发 的处理客户端的请求; (I/O 多路复用机制)
  3. Redis 服务中运行的绝大多数操做的 性能瓶颈都不是 CPU

强烈推荐 各位亲看一下这篇文章:

Redis 为何这么快?

简单总结:

  1. 纯内存操做:读取不须要进行磁盘 I/O,因此比传统数据库要快上很多; (但不要有误区说磁盘就必定慢,例如 Kafka 就是使用磁盘顺序读取但仍然较快)
  2. 单线程,无锁竞争:这保证了没有线程的上下文切换,不会由于多线程的一些操做而下降性能;
  3. 多路 I/O 复用模型,非阻塞 I/O:采用多路 I/O 复用技术可让单个线程高效的处理多个网络链接请求(尽可能减小网络 IO 的时间消耗);
  4. 高效的数据结构,加上底层作了大量优化:Redis 对于底层的数据结构和内存占用作了大量的优化,例如不一样长度的字符串使用不一样的结构体表示,HyperLogLog 的密集型存储结构等等..

2、数据结构篇

简述一下 Redis 经常使用数据结构及实现?

首先在 Redis 内部会使用一个 RedisObject 对象来表示全部的 keyvalue

其次 Redis 为了 平衡空间和时间效率,针对 value 的具体类型在底层会采用不一样的数据结构来实现,下图展现了他们之间的映射关系:(好像乱糟糟的,但至少能看清楚..)

Redis 的 SDS 和 C 中字符串相比有什么优点?

先简单总结一下

C 语言使用了一个长度为 N+1 的字符数组来表示长度为 N 的字符串,而且字符数组最后一个元素老是 \0,这种简单的字符串表示方式 不符合 Redis 对字符串在安全性、效率以及功能方面的要求

再来讲 C 语言字符串的问题

这样简单的数据结构可能会形成如下一些问题:

  • 获取字符串长度为 O(N) 级别的操做 → 由于 C 不保存数组的长度,每次都须要遍历一遍整个数组;
  • 不能很好的杜绝 缓冲区溢出/内存泄漏 的问题 → 跟上述问题缘由同样,若是执行拼接 or 缩短字符串的操做,若是操做不当就很容易形成上述问题;
  • C 字符串 只能保存文本数据 → 由于 C 语言中的字符串必须符合某种编码(好比 ASCII),例如中间出现的 '\0' 可能会被断定为提早结束的字符串而识别不了;

Redis 如何解决的 | SDS 的优点

若是去看 Redis 的源码 sds.h/sdshdr 文件,你会看到 SDS 完整的实现细节,这里简单来讲一下 Redis 如何解决的:

  1. 多增长 len 表示当前字符串的长度:这样就能够直接获取长度了,复杂度 O(1);
  2. 自动扩展空间:当 SDS 须要对字符串进行修改时,首先借助于 lenalloc 检查空间是否知足修改所需的要求,若是空间不够的话,SDS 会自动扩展空间,避免了像 C 字符串操做中的覆盖状况;
  3. 有效下降内存分配次数:C 字符串在涉及增长或者清除操做时会改变底层数组的大小形成从新分配,SDS 使用了 空间预分配惰性空间释放 机制,简单理解就是每次在扩展时是成倍的多分配的,在缩容是也是先留着并不正式归还给 OS;
  4. 二进制安全:C 语言字符串只能保存 ascii 码,对于图片、音频等信息没法保存,SDS 是二进制安全的,写入什么读取就是什么,不作任何过滤和限制;

字典是如何实现的?Rehash 了解吗?

先整体聊一下 Redis 中的字典

字典是 Redis 服务器中出现最为频繁的复合型数据结构。除了 hash 结构的数据会用到字典外,整个 Redis 数据库的全部 keyvalue 也组成了一个 全局字典,还有带过时时间的 key 也是一个字典。(存储在 RedisDb 数据结构中)

说明字典内部结构和 rehash

Redis 中的字典至关于 Java 中的 HashMap,内部实现也差很少相似,都是经过 "数组 + 链表"链地址法 来解决部分 哈希冲突,同时这样的结构也吸取了两种不一样数据结构的优势。

字典结构内部包含 两个 hashtable,一般状况下只有一个 hashtable 有值,可是在字典扩容缩容时,须要分配新的 hashtable,而后进行 渐进式搬迁 (rehash),这时候两个 hashtable 分别存储旧的和新的 hashtable,待搬迁结束后,旧的将被删除,新的 hashtable 取而代之。

扩缩容的条件

正常状况下,当 hash 表中 元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是 原数组大小的 2 倍。不过若是 Redis 正在作 bgsave(持久化命令),为了减小内存也得过多分离,Redis 尽可能不去扩容,可是若是 hash 表很是满了,达到了第一维数组长度的 5 倍了,这个时候就会 强制扩容

当 hash 表由于元素逐渐被删除变得愈来愈稀疏时,Redis 会对 hash 表进行缩容来减小 hash 表的第一维数组空间占用。所用的条件是 元素个数低于数组长度的 10%,缩容不会考虑 Redis 是否在作 bgsave

跳跃表是如何实现的?原理?

这是 Redis 中比较重要的一个数据结构,建议阅读 以前写过的文章,里面详细介绍了原理和一些细节:

HyperLogLog 有了解吗?

建议阅读 以前的系列文章:

布隆过滤器有了解吗?

建议阅读 以前的系列文章:

GeoHash 了解吗?

建议阅读 以前的系列文章:

压缩列表了解吗?

这是 Redis 为了节约内存 而使用的一种数据结构,zsethash 容器对象会在元素个数较少的时候,采用压缩列表(ziplist)进行存储。压缩列表是 一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。

由于以前本身也没有学习过,因此找了一篇比较比如较容易理解的文章:

快速列表 quicklist 了解吗?

Redis 早期版本存储 list 列表数据结构使用的是压缩列表 ziplist 和普通的双向链表 linkedlist,也就是说当元素少时使用 ziplist,当元素多时用 linkedlist。但考虑到链表的附加空间相对较高,prevnext 指针就要占去 16 个字节(64 位操做系统占用 8 个字节),另外每一个节点的内存都是单独分配,会家具内存的碎片化,影响内存管理效率。

后来 Redis 新版本(3.2)对列表数据结构进行了改造,使用 quicklist 代替了 ziplistlinkedlist

同上..建议阅读一下如下的文章:

Stream 结构有了解吗?

Redis Stream 从概念上来讲,就像是一个 仅追加内容消息链表,把全部加入的消息都一个一个串起来,每一个消息都有一个惟一的 ID 和内容,这很简单,让它复杂的是从 Kafka 借鉴的另外一种概念:消费者组(Consumer Group) (思路一致,实现不一样)

上图就展现了一个典型的 Stream 结构。每一个 Stream 都有惟一的名称,它就是 Redis 的 key,在咱们首次使用 xadd 指令追加消息时自动建立。咱们对图中的一些概念作一下解释:

  • Consumer Group:消费者组,能够简单当作记录流状态的一种数据结构。消费者既能够选择使用 XREAD 命令进行 独立消费,也能够多个消费者同时加入一个消费者组进行 组内消费。同一个消费者组内的消费者共享全部的 Stream 信息, 同一条消息只会有一个消费者消费到,这样就能够应用在分布式的应用场景中来保证消息的惟一性。
  • last_delivered_id:用来表示消费者组消费在 Stream 上 消费位置 的游标信息。每一个消费者组都有一个 Stream 内 惟一的名称,消费者组不会自动建立,须要使用 XGROUP CREATE 指令来显式建立,而且须要指定从哪个消息 ID 开始消费,用来初始化 last_delivered_id 这个变量。
  • pending_ids:每一个消费者内部都有的一个状态变量,用来表示 已经 被客户端 获取,可是 尚未 ack 的消息。记录的目的是为了 保证客户端至少消费了消息一次,而不会在网络传输的中途丢失而没有对消息进行处理。若是客户端没有 ack,那么这个变量里面的消息 ID 就会愈来愈多,一旦某个消息被 ack,它就会对应开始减小。这个变量也被 Redis 官方称为 PEL (Pending Entries List)

Stream 消息太多怎么办?

很容易想到,要是消息积累太多,Stream 的链表岂不是很长,内容会不会爆掉就是个问题了。xdel 指令又不会删除消息,它只是给消息作了个标志位。

Redis 天然考虑到了这一点,因此它提供了一个定长 Stream 功能。在 xadd 的指令提供一个定长长度 maxlen,就能够将老的消息干掉,确保最多不超过指定长度,使用起来也很简单:

> XADD mystream MAXLEN 2 * value 1
1526654998691-0
> XADD mystream MAXLEN 2 * value 2
1526654999635-0
> XADD mystream MAXLEN 2 * value 3
1526655000369-0
> XLEN mystream
(integer) 2
> XRANGE mystream - +
1) 1) 1526654999635-0
2) 1) "value"
2) "2"
2) 1) 1526655000369-0
2) 1) "value"
2) "3"
复制代码

若是使用 MAXLEN 选项,当 Stream 的达到指定长度后,老的消息会自动被淘汰掉,所以 Stream 的大小是恒定的。目前尚未选项让 Stream 只保留给定数量的条目,由于为了一致地运行,这样的命令必须在很长一段时间内阻塞以淘汰消息。(例如在添加数据的高峰期间,你不得不长暂停来淘汰旧消息和添加新的消息)

另外使用 MAXLEN 选项的花销是很大的,Stream 为了节省内存空间,采用了一种特殊的结构表示,而这种结构的调整是须要额外的花销的。因此咱们能够使用一种带有 ~ 的特殊命令:

XADD mystream MAXLEN ~ 1000 * ... entry fields here ...
复制代码

它会基于当前的结构合理地对节点执行裁剪,来保证至少会有 1000 条数据,多是 1010 也多是 1030

PEL 是如何避免消息丢失的?

在客户端消费者读取 Stream 消息时,Redis 服务器将消息回复给客户端的过程当中,客户端忽然断开了链接,消息就丢失了。可是 PEL 里已经保存了发出去的消息 ID,待客户端从新连上以后,能够再次收到 PEL 中的消息 ID 列表。不过此时 xreadgroup 的起始消息 ID 不能为参数 > ,而必须是任意有效的消息 ID,通常将参数设为 0-0,表示读取全部的 PEL 消息以及自 last_delivered_id 以后的新消息。

和 Kafka 对比起来呢?

Redis 基于内存存储,这意味着它会比基于磁盘的 Kafka 快上一些,也意味着使用 Redis 咱们 不能长时间存储大量数据。不过若是您想以 最小延迟 实时处理消息的话,您能够考虑 Redis,可是若是 消息很大而且应该重用数据 的话,则应该首先考虑使用 Kafka。

另外从某些角度来讲,Redis Stream 也更适用于小型、廉价的应用程序,由于 Kafka 相对来讲更难配置一些。

推荐阅读 以前的系列文章,里面 也对 Pub/ Sub 作了详细的描述

3、持久化篇

什么是持久化?

先简单谈一谈是什么

Redis 的数据 所有存储内存 中,若是 忽然宕机,数据就会所有丢失,所以必须有一套机制来保证 Redis 的数据不会由于故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。

解释一下持久化发生了什么

咱们来稍微考虑一下 Redis 做为一个 "内存数据库" 要作的关于持久化的事情。一般来讲,从客户端发起请求开始,到服务器真实地写入磁盘,须要发生以下几件事情:

详细版 的文字描述大概就是下面这样:

  1. 客户端向数据库 发送写命令 (数据在客户端的内存中)
  2. 数据库 接收 到客户端的 写请求 (数据在服务器的内存中)
  3. 数据库 调用系统 API 将数据写入磁盘 (数据在内核缓冲区中)
  4. 操做系统将 写缓冲区 传输到 磁盘控控制器 (数据在磁盘缓存中)
  5. 操做系统的磁盘控制器将数据 写入实际的物理媒介(数据在磁盘中)

分析如何保证持久化安全

若是咱们故障仅仅涉及到 软件层面 (该进程被管理员终止或程序崩溃) 而且没有接触到内核,那么在 上述步骤 3 成功返回以后,咱们就认为成功了。即便进程崩溃,操做系统仍然会帮助咱们把数据正确地写入磁盘。

若是咱们考虑 停电/ 火灾更具灾难性 的事情,那么只有在完成了第 5 步以后,才是安全的。

机房”火了“
机房”火了“

因此咱们能够总结得出数据安全最重要的阶段是:步骤3、4、五,即:

  • 数据库软件调用写操做将用户空间的缓冲区转移到内核缓冲区的频率是多少?
  • 内核多久从缓冲区取数据刷新到磁盘控制器?
  • 磁盘控制器多久把数据写入物理媒介一次?
  • 注意: 若是真的发生灾难性的事件,咱们能够从上图的过程当中看到,任何一步均可能被意外打断丢失,因此只能 尽量地保证 数据的安全,这对于全部数据库来讲都是同样的。

咱们从 第三步 开始。Linux 系统提供了清晰、易用的用于操做文件的 POSIX file API20 多年过去,仍然还有不少人对于这一套 API 的设计津津乐道,我想其中一个缘由就是由于你光从 API 的命名就可以很清晰地知道这一套 API 的用途:

int open(const char *path, int oflag, .../*,mode_t mode */);
int close (int filedes);int remove( const char *fname );
ssize_t write(int fildes, const void *buf, size_t nbyte);
ssize_t read(int fildes, void *buf, size_t nbyte);
复制代码

因此,咱们有很好的可用的 API 来完成 第三步,可是对于成功返回以前,咱们对系统调用花费的时间没有太多的控制权。

而后咱们来讲说 第四步。咱们知道,除了早期对电脑特别了解那帮人 (操做系统就这帮人搞的),实际的物理硬件都不是咱们可以 直接操做 的,都是经过 操做系统调用 来达到目的的。为了防止过慢的 I/O 操做拖慢整个系统的运行,操做系统层面作了不少的努力,譬如说 上述第四步 提到的 写缓冲区,并非全部的写操做都会被当即写入磁盘,而是要先通过一个缓冲区,默认状况下,Linux 将在 30 秒 后实际提交写入。

可是很明显,30 秒 并非 Redis 可以承受的,这意味着,若是发生故障,那么最近 30 秒内写入的全部数据均可能会丢失。幸亏 PROSIX API 提供了另外一个解决方案:fsync,该命令会 强制 内核将 缓冲区 写入 磁盘,但这是一个很是消耗性能的操做,每次调用都会 阻塞等待 直到设备报告 IO 完成,因此通常在生产环境的服务器中,Redis 一般是每隔 1s 左右执行一次 fsync 操做。

到目前为止,咱们了解到了如何控制 第三步第四步,可是对于 第五步,咱们 彻底没法控制。也许一些内核实现将试图告诉驱动实际提交物理介质上的数据,或者控制器可能会为了提升速度而从新排序写操做,不会尽快将数据真正写到磁盘上,而是会等待几个多毫秒。这彻底是咱们没法控制的。

普通人简单说一下第一条就过了,若是你详细地对后面两方面 侃侃而谈,那面试官就会对你刮目相看了。

Redis 中的两种持久化方式?

方式一:快照

Redis 快照 是最简单的 Redis 持久性模式。当知足特定条件时,它将生成数据集的时间点快照,例如,若是先前的快照是在 2 分钟前建立的,而且如今已经至少有 100 次新写入,则将建立一个新的快照。此条件能够由用户配置 Redis 实例来控制,也能够在运行时修改而无需从新启动服务器。快照做为包含整个数据集的单个 .rdb 文件生成。

方式二:AOF

快照不是很持久。若是运行 Redis 的计算机中止运行,电源线出现故障或者您 kill -9 的实例意外发生,则写入 Redis 的最新数据将丢失。尽管这对于某些应用程序可能不是什么大问题,但有些使用案例具备充分的耐用性,在这些状况下,快照并非可行的选择。

AOF(Append Only File - 仅追加文件) 它的工做方式很是简单:每次执行 修改内存 中数据集的写操做时,都会 记录 该操做。假设 AOF 日志记录了自 Redis 实例建立以来 全部的修改性指令序列,那么就能够经过对一个空的 Redis 实例 顺序执行全部的指令,也就是 「重放」,来恢复 Redis 当前实例的内存数据结构的状态。

Redis 4.0 的混合持久化

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

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

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

关于两种持久化方式的更多细节 (原理) 能够参考:

RDB 和 AOF 各自有什么优缺点?

RDB | 优势

  1. 只有一个文件 dump.rdb方便持久化
  2. 容灾性好,一个文件能够保存到安全的磁盘。
  3. 性能最大化fork 子进程来完成写操做,让主进程继续处理命令,因此使 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操做,保证了 Redis 的高性能
  4. 相对于数据集大时,比 AOF 的 启动效率 更高。

RDB | 缺点

  1. 数据安全性低。RDB 是间隔一段时间进行持久化,若是持久化之间 Redis 发生故障,会发生数据丢失。因此这种方式更适合数据要求不严谨的时候;

AOF | 优势

  1. 数据安全,aof 持久化能够配置 appendfsync 属性,有 always,每进行一次命令操做就记录到 aof 文件中一次。
  2. 经过 append 模式写文件,即便中途服务器宕机,能够经过 redis-check-aof 工具解决数据一致性问题。
  3. AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 以前(文件过大时会对命令 进行合并重写),能够删除其中的某些命令(好比误操做的 flushall)

AOF | 缺点

  1. AOF 文件比 RDB 文件大,且 恢复速度慢
  2. 数据集大 的时候,比 rdb 启动效率低

两种方式如何选择?

  • 通常来讲, 若是想达到足以媲美 PostgreSQL 的 数据安全性,你应该 同时使用两种持久化功能。在这种状况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,由于在一般状况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
  • 若是你很是关心你的数据, 但仍然 能够承受数分钟之内的数据丢失,那么你能够 只使用 RDB 持久化
  • 有不少用户都只使用 AOF 持久化,但并不推荐这种方式,由于定时生成 RDB 快照(snapshot)很是便于进行数据库备份, 而且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此以外,使用 RDB 还能够避免 AOF 程序的 bug。
  • 若是你只但愿你的数据在服务器运行的时候存在,你也能够不使用任何持久化方式。

Redis 的数据恢复

Redis 的数据恢复有着以下的优先级:

  1. 若是只配置 AOF ,重启时加载 AOF 文件恢复数据;
  2. 若是同时配置了 RDB 和 AOF ,启动只加载 AOF 文件恢复数据;
  3. 若是只配置 RDB,启动将加载 dump 文件恢复数据。

拷贝 AOF 文件到 Redis 的数据目录,启动 redis-server AOF 的数据恢复过程:Redis 虚拟一个客户端,读取 AOF 文件恢复 Redis 命令和参数,而后执行命令从而恢复数据,这些过程主要在 loadAppendOnlyFile() 中实现。

拷贝 RDB 文件到 Redis 的数据目录,启动 redis-server 便可,由于 RDB 文件和重启前保存的是真实数据而不是命令状态和参数。

4、集群篇

主从同步了解吗?

主从复制,是指将一台 Redis 服务器的数据,复制到其余的 Redis 服务器。前者称为 主节点(master),后者称为 从节点(slave)。且数据的复制是 单向 的,只能由主节点到从节点。Redis 主从复制支持 主从同步从从同步 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。

主从复制主要的做用

  • 数据冗余: 主从复制实现了数据的热备份,是持久化以外的一种数据冗余方式。
  • 故障恢复: 当主节点出现问题时,能够由从节点提供服务,实现快速的故障恢复 (其实是一种服务的冗余)
  • 负载均衡: 在主从复制的基础上,配合读写分离,能够由主节点提供写服务,由从节点提供读服务 (即写 Redis 数据时应用链接主节点,读 Redis 数据时应用链接从节点),分担服务器负载。尤为是在写少读多的场景下,经过多个从节点分担读负载,能够大大提升 Redis 服务器的并发量。
  • 高可用基石: 除了上述做用之外,主从复制仍是哨兵和集群可以实施的 基础,所以说主从复制是 Redis 高可用的基础。

实现原理

为了节省篇幅,我把主要的步骤都 浓缩 在了上图中,其实也能够 简化成三个阶段:准备阶段-数据同步阶段-命令传播阶段

更多细节 推荐阅读 以前的系列文章,不只有原理讲解,还有实战环节:

哨兵模式了解吗?

上图 展现了一个典型的哨兵架构图,它由两部分组成,哨兵节点和数据节点:

  • 哨兵节点: 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据;
  • 数据节点: 主节点和从节点都是数据节点;

在复制的基础上,哨兵实现了 自动化的故障恢复 功能,下方是官方对于哨兵功能的描述:

  • 监控(Monitoring): 哨兵会不断地检查主节点和从节点是否运做正常。
  • 自动故障转移(Automatic failover):主节点 不能正常工做时,哨兵会开始 自动故障转移操做,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其余从节点改成复制新的主节点。
  • 配置提供者(Configuration provider): 客户端在初始化时,经过链接哨兵来得到当前 Redis 服务的主节点地址。
  • 通知(Notification): 哨兵能够将故障转移的结果发送给客户端。

其中,监控和自动故障转移功能,使得哨兵能够及时发现主节点故障并完成转移。而配置提供者和通知功能,则须要在与客户端的交互中才能体现。

新的主服务器是怎样被挑选出来的?

故障转移操做的第一步 要作的就是在已下线主服务器属下的全部从服务器中,挑选出一个状态良好、数据完整的从服务器,而后向这个从服务器发送 slaveof no one 命令,将这个从服务器转换为主服务器。可是这个从服务器是怎么样被挑选出来的呢?

简单来讲 Sentinel 使用如下规则来选择新的主服务器:

  1. 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被 淘汰
  2. 在失效主服务器属下的从服务器当中, 那些与失效主服务器链接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被 淘汰
  3. 经历了以上两轮淘汰以后 剩下来的从服务器中, 咱们选出 复制偏移量(replication offset)最大 的那个 从服务器 做为新的主服务器;若是复制偏移量不可用,或者从服务器的复制偏移量相同,那么 带有最小运行 ID 的那个从服务器成为新的主服务器。

更多细节 推荐阅读 以前的系列文章,不只有原理讲解,还有实战环节:

Redis 集群使用过吗?原理?

上图 展现了 Redis Cluster 典型的架构图,集群中的每个 Redis 节点都 互相两两相连,客户端任意 直连 到集群中的 任意一台,就能够对其余 Redis 节点进行 读写 的操做。

基本原理

Redis 集群中内置了 16384 个哈希槽。当客户端链接到 Redis 集群以后,会同时获得一份关于这个 集群的配置信息,当客户端具体对某一个 key 值进行操做时,会计算出它的一个 Hash 值,而后把结果对 16384 求余数,这样每一个 key 都会对应一个编号在 0-16383 之间的哈希槽,Redis 会根据节点数量 大体均等 的将哈希槽映射到不一样的节点。

再结合集群的配置信息就可以知道这个 key 值应该存储在哪个具体的 Redis 节点中,若是不属于本身管,那么就会使用一个特殊的 MOVED 命令来进行一个跳转,告诉客户端去链接这个节点以获取数据:

GET x
-MOVED 3999 127.0.0.1:6381
复制代码

MOVED 指令第一个参数 3999key 对应的槽位编号,后面是目标节点地址,MOVED 命令前面有一个减号,表示这是一个错误的消息。客户端在收到 MOVED 指令后,就当即纠正本地的 槽位映射表,那么下一次再访问 key 时就可以到正确的地方去获取了。

集群的主要做用

  1. 数据分区: 数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点, 一方面 突破了 Redis 单机内存大小的限制, 存储容量大大增长另外一方面 每一个主节点均可以对外提供读服务和写服务, 大大提升了集群的响应能力。Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有说起,例如,若是单机内存太大, bgsavebgrewriteaoffork 操做可能致使主进程阻塞,主从环境下主机切换时可能致使从节点长时间没法提供服务,全量复制阶段主节点的复制缓冲区可能溢出……
  2. 高可用: 集群支持主从复制和主节点的 自动故障转移 (与哨兵相似),当任一节点发生故障时,集群仍然能够对外提供服务。

集群中数据如何分区?

Redis 采用方案三。

方案一:哈希值 % 节点数

哈希取余分区思路很是简单:计算 key 的 hash 值,而后对节点数量进行取余,从而决定数据映射到哪一个节点上。

不过该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中全部的数据都须要 从新计算映射关系,引起大规模数据迁移。

方案二:一致性哈希分区

一致性哈希算法将 整个哈希值空间 组织成一个虚拟的圆环,范围是 [0 - 232 - 1],对于每个数据,根据 key 计算 hash 值,确数据在环上的位置,而后今后位置沿顺时针行走,找到的第一台服务器就是其应该映射到的服务器:

与哈希取余分区相比,一致性哈希分区将 增减节点的影响限制在相邻节点。以上图为例,若是在 node1node2 之间增长 node5,则只有 node2 中的一部分数据会迁移到 node5;若是去掉 node2,则原 node2 中的数据只会迁移到 node4 中,只有 node4 会受影响。

一致性哈希分区的主要问题在于,当 节点数量较少 时,增长或删减节点,对单个节点的影响可能很大,形成数据的严重不平衡。仍是以上图为例,若是去掉 node2node4 中的数据由总数据的 1/4 左右变为 1/2 左右,与其余节点相比负载太高。

方案三:带有虚拟节点的一致性哈希分区

该方案在 一致性哈希分区的基础上,引入了 虚拟节点 的概念。Redis 集群使用的即是该方案,其中的虚拟节点称为 槽(slot)。槽是介于数据和实际节点之间的虚拟概念,每一个实际节点包含必定数量的槽,每一个槽包含哈希值在必定范围内的数据。

在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽 解耦数据和实际节点 之间的关系,增长或删除节点对系统的影响很小。仍以上图为例,系统中有 4 个实际节点,假设为其分配 16 个槽(0-15);

  • 槽 0-3 位于 node1;4-7 位于 node2;以此类推....

若是此时删除 node2,只须要将槽 4-7 从新分配便可,例如槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4;能够看出删除 node2 后,数据在其余节点的分布仍然较为均衡。

节点之间的通讯机制了解吗?

集群的创建离不开节点之间的通讯,例如咱们在 快速体验 中刚启动六个集群节点以后经过 redis-cli 命令帮助咱们搭建起来了集群,实际上背后每一个集群之间的两两链接是经过了 CLUSTER MEET <ip> <port> 命令发送 MEET 消息完成的,下面咱们展开详细说说。

两个端口

哨兵系统 中,节点分为 数据节点哨兵节点:前者存储数据,后者实现额外的控制功能。在 集群 中,没有数据节点与非数据节点之分:全部的节点都存储数据,也都参与集群状态的维护。为此,集群中的每一个节点,都提供了两个 TCP 端口:

  • 普通端口: 即咱们在前面指定的端口 (7000等)。普通端口主要用于为客户端提供服务 (与单机节点相似);但在节点间数据迁移时也会使用。
  • 集群端口: 端口号是普通端口 + 10000 (10000是固定值,没法改变),如 7000 节点的集群端口为 17000集群端口只用于节点之间的通讯,如搭建集群、增减节点、故障转移等操做时节点间的通讯;不要使用客户端链接集群接口。为了保证集群能够正常工做,在配置防火墙时,要同时开启普通端口和集群端口。

Gossip 协议

节点间通讯,按照通讯协议能够分为几种类型:单对单、广播、Gossip 协议等。重点是广播和 Gossip 的对比。

  • 广播是指向集群内全部节点发送消息。 优势 是集群的收敛速度快(集群收敛是指集群内全部节点得到的集群信息是一致的), 缺点 是每条消息都要发送给全部节点,CPU、带宽等消耗较大。
  • Gossip 协议的特色是:在节点数量有限的网络中, 每一个节点都 “随机” 的与部分节点通讯 (并非真正的随机,而是根据特定的规则选择通讯的节点),通过一番杂乱无章的通讯,每一个节点的状态很快会达到一致。Gossip 协议的 优势 有负载 (比广播) 低、去中心化、容错性高 (由于通讯有冗余) 等; 缺点 主要是集群的收敛速度慢。

消息类型

集群中的节点采用 固定频率(每秒10次)定时任务 进行通讯相关的工做:判断是否须要发送消息及消息类型、肯定接收节点、发送消息等。若是集群状态发生了变化,如增减节点、槽状态变动,经过节点间的通讯,全部节点会很快得知整个集群的状态,使集群收敛。

节点间发送的消息主要分为 5 种:meet 消息ping 消息pong 消息fail 消息publish 消息。不一样的消息类型,通讯协议、发送的频率和时机、接收节点的选择等是不一样的:

  • MEET 消息: 在节点握手阶段,当节点收到客户端的 CLUSTER MEET 命令时,会向新加入的节点发送 MEET 消息,请求新节点加入到当前集群;新节点收到 MEET 消息后会回复一个 PONG 消息。
  • PING 消息: 集群里每一个节点每秒钟会选择部分节点发送 PING 消息,接收者收到消息后会回复一个 PONG 消息。 PING 消息的内容是自身节点和部分其余节点的状态信息,做用是彼此交换信息,以及检测节点是否在线。 PING 消息使用 Gossip 协议发送,接收节点的选择兼顾了收敛速度和带宽成本, 具体规则以下:(1)随机找 5 个节点,在其中选择最久没有通讯的 1 个节点;(2)扫描节点列表,选择最近一次收到 PONG 消息时间大于 cluster_node_timeout / 2 的全部节点,防止这些节点长时间未更新。
  • PONG消息: PONG 消息封装了自身状态数据。能够分为两种: 第一种 是在接到 MEET/PING 消息后回复的 PONG 消息; 第二种 是指节点向集群广播 PONG 消息,这样其余节点能够获知该节点的最新信息,例如故障恢复后新的主节点会广播 PONG 消息。
  • FAIL 消息: 当一个主节点判断另外一个主节点进入 FAIL 状态时,会向集群广播这一 FAIL 消息;接收节点会将这一 FAIL 消息保存起来,便于后续的判断。
  • PUBLISH 消息: 节点收到 PUBLISH 命令后,会先执行该命令,而后向集群广播这一消息,接收节点也会执行该 PUBLISH 命令。

集群数据如何存储的有了解吗?

节点须要专门的数据结构来存储集群的状态。所谓集群的状态,是一个比较大的概念,包括:集群是否处于上线状态、集群中有哪些节点、节点是否可达、节点的主从状态、槽的分布……

节点为了存储集群状态而提供的数据结构中,最关键的是 clusterNodeclusterState 结构:前者记录了一个节点的状态,后者记录了集群做为一个总体的状态。

clusterNode 结构

clusterNode 结构保存了 一个节点的当前状态,包括建立时间、节点 id、ip 和端口号等。每一个节点都会用一个 clusterNode 结构记录本身的状态,并为集群内全部其余节点都建立一个 clusterNode 结构来记录节点状态。

下面列举了 clusterNode 的部分字段,并说明了字段的含义和做用:

typedef struct clusterNode {
//节点建立时间
mstime_t ctime;
//节点id
char name[REDIS_CLUSTER_NAMELEN];
//节点的ip和端口号
char ip[REDIS_IP_STR_LEN];
int port;
//节点标识:整型,每一个bit都表明了不一样状态,如节点的主从状态、是否在线、是否在握手等
int flags;
//配置纪元:故障转移时起做用,相似于哨兵的配置纪元
uint64_t configEpoch;
//槽在该节点中的分布:占用16384/8个字节,16384个比特;每一个比特对应一个槽:比特值为1,则该比特对应的槽在节点中;比特值为0,则该比特对应的槽不在节点中
unsigned char slots[16384/8];
//节点中槽的数量
int numslots;
…………
} clusterNode;
复制代码

除了上述字段,clusterNode 还包含节点链接、主从复制、故障发现和转移须要的信息等。

clusterState 结构

clusterState 结构保存了在当前节点视角下,集群所处的状态。主要字段包括:

typedef struct clusterState {
//自身节点
clusterNode *myself;
//配置纪元
uint64_t currentEpoch;
//集群状态:在线仍是下线
int state;
//集群中至少包含一个槽的节点数量
int size;
//哈希表,节点名称->clusterNode节点指针
dict *nodes;
//槽分布信息:数组的每一个元素都是一个指向clusterNode结构的指针;若是槽尚未分配给任何节点,则为NULL
clusterNode *slots[16384];
…………
} clusterState;
复制代码

除此以外,clusterState 还包括故障转移、槽迁移等须要的信息。

5、其余问题

Redis 如何实现分布式锁?

推荐阅读 以前的系列文章: Redis(3)——分布式锁深刻探究 - www.wmyskxz.com/2020/03/01/…

Redis 过时键的删除策略?

简单描述

先抛开 Redis 想一下几种可能的删除策略:

  1. 定时删除:在设置键的过时时间的同时,建立一个定时器 timer). 让定时器在键的过时时间来临时,当即执行对键的删除操做。
  2. 惰性删除:听任键过时无论,可是每次从键空间中获取键时,都检查取得的键是否过时,若是过时的话,就删除该键;若是没有过时,就返回该键。
  3. 按期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过时键。至于要删除多少过时键,以及要检查多少个数据库,则由算法决定。

在上述的三种策略中定时删除和按期删除属于不一样时间粒度的 主动删除,惰性删除属于 被动删除

三种策略都有各自的优缺点

  1. 定时删除对内存使用率有优点,可是对 CPU 不友好;
  2. 惰性删除对内存不友好,若是某些键值对一直不被使用,那么会形成必定量的内存浪费;
  3. 按期删除是定时删除和惰性删除的折中。

Redis 中的实现

Reids 采用的是 惰性删除和定时删除 的结合,通常来讲能够借助最小堆来实现定时器,不过 Redis 的设计考虑到时间事件的有限种类和数量,使用了无序链表存储时间事件,这样若是在此基础上实现定时删除,就意味着 O(N) 遍历获取最近须要删除的数据。

Redis 的淘汰策略有哪些?

Redis 有六种淘汰策略

策略 描述
volatile-lru 从已设置过时时间的 KV 集中优先对最近最少使用(less recently used)的数据淘汰
volitile-ttl 从已设置过时时间的 KV 集中优先对剩余时间短(time to live)的数据淘汰
volitile-random 从已设置过时时间的 KV 集中随机选择数据淘汰
allkeys-lru 从全部 KV 集中优先对最近最少使用(less recently used)的数据淘汰
allKeys-random 从全部 KV 集中随机选择数据淘汰
noeviction 不淘汰策略,若超过最大内存,返回错误信息

4.0 版本后增长如下两种

  • volatile-lfu:从已设置过时时间的数据集(server.db[i].expires)中挑选最不常用的数据淘汰
  • allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不常用的 key

Redis常见性能问题和解决方案?

  1. Master 最好不要作任何持久化工做,包括内存快照和 AOF 日志文件,特别是不要启用内存快照作持久化。
  2. 若是数据比较关键,某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
  3. 为了主从复制的速度和链接的稳定性,Slave 和 Master 最好在同一个局域网内。
  4. 尽可能避免在压力较大的主库上增长从库。
  5. Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,致使服务 load 太高,出现短暂服务暂停现象。
  6. 为了 Master 的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现 Slave 对 Master 的替换,也即,若是 Master 挂了,能够立马启用 Slave1 作 Master,其余不变。

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们所有找出来?

使用 keys 指令能够扫出指定模式的 key 列表。可是要注意 keys 指令会致使线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候能够使用 scan 指令,scan 指令能够无阻塞的提取出指定模式的 key 列表,可是会有必定的重复几率,在客户端作一次去重就能够了,可是总体所花费的时间会比直接用 keys 指令长。

More..

参考资料

  1. 3w字深度好文|Redis面试全攻略,读完这个就能够和面试官大战几个回合了 - mp.weixin.qq.com/s/f9N13fnyT…
  2. 大厂面试!我和面试官之间关于Redis的一场对弈! - [https://mp.weixin.qq.com/s/DHTPSfmWTZpdTmlytzLz1g](https://mp.weixin.qq.com/s/DHTPSfmWTZpdTmlytzLz1g
  3. Redis面试题(2020最新版) - blog.csdn.net/ThinkWon/ar…
  • 本文已收录至个人 Github 程序员成长系列 【More Than Java】,学习,不止 Code,欢迎 star:github.com/wmyskxz/Mor…
  • 我的公众号 :wmyskxz, 我的独立域名博客:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长!

很是感谢各位人才能 看到这里,若是以为本篇文章写得不错,以为 「我没有三颗心脏」有点东西 的话,求点赞,求关注,求分享,求留言!

创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见!

相关文章
相关标签/搜索