文章首发于公众号“蘑菇睡不着”,欢迎来访~
你们都知道 Redis 是一个内存数据库,数据都存储在内存中,这也是 Redis 很是快的缘由之一。虽然速度提上来了,可是若是数据一直放在内存中,是很是容易丢失的。好比 服务器关闭或宕机了,内存中的数据就木有了。为了解决这一问题,Redis 提供了 持久化 机制。分别是 RDB 以及 AOF 持久化。c++
RDB 持久化能够在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。redis
RDB 在重启保存了大数据集的实例比 AOF 快。数据库
有个两个 Redis 命令能够用于生成 RDB 文件,一个是 SAVE,另外一个是 BGSAVE。
SAVE 命令会阻塞 Redis 服务器进程,直到 RDB 文件建立完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。数组
> SAVE // 一直等到 RDB 文件建立完毕 OK
和 SAVE 命令直接阻塞服务器进程不一样的是,BGSAVE 命令会派生出一个子进程,而后由子进程负责建立 RDB 文件,服务器进程(父进程)继续处理命令进程。
执行fork的时候操做系统(类Unix操做系统)会使用写时复制(copy-on-write)策略,即fork函数发生的一刻父子进程共享同一内存数据,当父进程要更改其中某片数据时(如执行一个写命令 ),操做系统会将该片数据复制一份以保证子进程的数据不受影响,因此新的RDB文件存储的是执行fork一刻的内存数据。安全
> BGSAVE // 派生子进程,并由子进程建立 RDB 文件 Background saving started
生成 RDB 文件由两种方式:一种是手动,就是上边介绍的用命令的方式;另外一种是自动的方式。
接下来详细介绍一下自动生成 RDB 文件的流程。
Redis 容许用户经过设置服务器配置的 save 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令。
用户能够经过在 redis.conf 配置文件中的 SNAPSHOTTING 下 save 选项设置多个保存条件,但只要其中任意一个条件被知足,服务器就会执行 BGSAEVE 命令。
如,如下配置:
save 900 1
save 300 10
save 60 10000
上边三个配置的含义是:服务器
若是没有手动去配置 save 选项,那么服务器会为 save 选项配置默认参数:
save 900 1
save 300 10
save 60 10000
接着,服务器就会根据 save 选项的配置,去设置服务器状态 redisServer 结构的 saveparams 属性:网络
struct redisServer{ // ... // 记录了保存条件的数组 struct saveparams *saveparams; // ... };
saveparams 属性是一个数组,数组中的每个元素都是一个 saveparam 结构,每一个 saveparam 结构都保存了一个 save 选项设置的保存条件:app
struct saveparam { // 秒数 time_t seconds; // 修改数 int changes; };
除了 saveparams 数组以外,服务器状态还维持着一个 dirty 计数器,以及一个 lastsave 属性;async
struct redisServer { // ... // 修改计数器 long long dirty; // 上一次执行保存时间 time_t lastsave; // ... }
Redis 的服务器周期性操做函数 serverCron 默认每隔 100 毫秒执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工做就是检查 save 选项所设置的保存条件是否已经知足,若是知足的话就执行 BGSAVE 命令。
Redis serverCron 源码解析以下:函数
程序会遍历并检查 saveparams 数组中的全部保存条件,只要有任意一个条件被知足,服务器就会执行 BGSAVE 命令。
下面是 rdbSaveBackground 的源码流程:
下图展现了一个完整 RDB 文件所包含的各个部分。
redis 文件的最开头是 REDIS 部分,这个部分的长度是 5 字节,保存着 “REDIS” 五个字符。经过这五个字符,程序能够在载入文件时,快速检查所载入的文件是否时 RDB 文件。
db_version 长度为 4 字节,他的值时一个字符串表示的整数,这个整数记录了 RDB 文件的版本号,好比 “0006” 就表明 RDB 文件的版本为第六版。
database 部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据:
EOF 常量的长度为 1 字节,这个常量标志着 RDB 文件正文内容的结束,当读入程序遇到这个值后,他知道全部数据库的全部键值对已经载入完毕了。
check_sum 是一个 8 字节长的无符号整数,保存着一个校验和,这个校验和时程序经过对 REDIS、db_version、database、EOF 四个部分的内容进行计算得出的。服务器在载入 RDB 文件时,会将载入数据所计算出的校验和与 check_sum 所记录的校验和进行对比,以此来检查 RDB 是否有出错或者损坏的状况。
举个例子:下图是一个 0 号数据库和 3 号数据库的 RDB 文件。第一个就是 “REDIS” 表示是一个 RDB 文件,以后的 “0006” 表示这是第六版的 REDIS 文件,而后是两个数据库,以后就是 EOF 结束标识符,最后就是 check_sum。
AOF持久化方式记录每次对服务器写的操做,当服务器重启的时候会从新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操做到文件末尾.Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大.
AOF持久化功能的实现能够分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令以后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾。
struct redisServer { // ... // AOF 缓冲区 sds aof_buf; // .. };
若是客户端向服务器发送如下命令:
> set KEY VALUE OK
那么服务器在执行这个 set 命令以后,会将如下协议内容追加到 aof_buf 缓冲区的末尾;
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端
的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像 serverCron 函数这样需
要定时运行的函数。
由于服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区
里面,因此在服务器每次结束一个事件循环以前,它都会调用 flushAppendOnlyFile 函数,考
虑是否须要将aof_buf缓冲区中的内容写入和保存到AOF文件里面,这个过程能够用如下伪代
码表示:
def eventLoop(): while True: #处理文件事件,接收命令请求以及发送命令回复 #处理命令请求时可能会有新内容被追加到 aof_buf缓冲区中 processFileEvents() #处理时间事件 processTimeEvents() #考虑是否要将 aof_buf中的内容写入和保存到 AOF文件里面 flushAppendOnlyFile()
flushAppendOnlyFile函数的行为由服务器配置的 appendfsync 选项的值来决定,各个不一样
值产生的行为以下表所示。
appendfsync 选项的值 | flushAppendOnlyFile 函数的行为 |
---|---|
always | 将 aof_buf 缓冲区中的全部内容写入并同步到 AOF 文件 |
everysec | 将 aof_buf 缓冲区中的全部内容写入到 AOF 文件,若是上次同步 AOF 文件的时间距离如今超过一秒钟,那么再次对 AOF 文件进行同步,而且这个同步操做是由一个线程专门负责执行的 |
no | 将 aof_buf 缓冲区中的全部内容写入到 AOF 文件,但并不对 AOF 文件进行同步,什么时候同步由操做系统来决定 |
若是用户没有主动为appendfsync选项设置值,那么appendfsync选项的默认值为everysec。
写到这里有的小伙伴可能会对上面说的写入和同步含义弄混,这里说一下:
写入:将 aof_buf 中的数据写入到 AOF 文件中。
同步:调用 fsync 以及 fdatasync 函数,将 AOF 文件中的数据保存到磁盘中。
通俗地讲就是,你要往一个文件写东西,写的过程就是写入,而同步则是将文件保存,数据落到磁盘上。
你们以前看文章的时候是否是大多都说 AOF 最多丢失一秒钟的数据,那是由于 redis AOF 默认是 everysec 策略,这个策略每秒执行一次,因此 AOF 持久化最多丢失一秒钟的数据。
由于AOF文件里面包含了重建数据库状态所需的全部写命令,因此服务器只要读入并从新执行一遍AOF文件里面保存的写命令,就能够还原服务器关闭以前的数据库状态。 Redis读取AOF文件并还原数据库状态的详细步骤以下:
由于AOF持久化是经过保存被执行的写命令来记录数据库状态的,因此随着服务器运行 时间的流逝,AOF文件中的内容会愈来愈多,文件的体积也会愈来愈大,若是不加以控制的 话,体积过大的AOF文件极可能对Redis服务器、甚至整个宿主计算机形成影响,而且AOF文 件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。
如 客户端执行了如下命令是:
> rpush list "A" "B" OK > rpush list "C" OK > rpush list "D" OK > rpush list "E" "F" OK
那么光是为了记录这个list键的状态,AOF文件就须要保存四条命令。
对于实际的应用程度来讲,写命令执行的次数和频率会比上面的简单示例要高得多,所 以形成的问题也会严重得多。 为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。经过该 功能,Redis服务器能够建立一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所 保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,因此新AOF文件 的体积一般会比旧AOF文件的体积要小得多。 在接下来的内容中,咱们将介绍AOF文件重写的实现原理,以及BGREWEITEAOF命令 的实现原理。
虽然Redis将生成新AOF文件替换旧AOF文件的功能命名为“AOF文件重写”,但实际上, AOF文件重写并不须要对现有的AOF文件进行任何读取、分析或者写入操做,这个功能是通 过读取服务器当前的数据库状态来实现的。
就像上面的状况,服务器彻底能够将这六条命令合并成一条。
> rpush list "A" "B" "C" "D" "E" "F"
除了上面列举的列表键以外,其余全部类型的键均可以用一样的方法去减小 AOF文件中的命令数量。首先从数据库中读取键如今的值,而后用一条命令去记录键值对,代替以前记录这个键值对的多条命令,这就是AOF重写功能的实现原理。
在实际中,为了不在执行命令时形成客户端输入缓冲区溢出,重写程序在处理列表、 哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数 量,若是元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那 么重写程序将使用多条命令来记录键的值,而不仅仅使用一条命令。 在目前版本中,REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值为64,这也就是 说,若是一个集合键包含了超过64个元素,那么重写程序会用多条SADD命令来记录这个集 合,而且每条命令设置的元素数量也为64个。
AOF 重写会执行大量的写操做,这样会影响主线程,因此redis AOF 重写放到了子进程去执行。这样能够达到两个目的:
可是有一个问题,当子进程重写数据时,主进程依然在处理新的数据,这也就会形成数据不一致状况。
为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在 服务器建立子进程以后开始使用,当Redis服务器执行完一个写命令以后,它会同时将这个写 命令发送给AOF缓冲区和AOF重写缓冲区,以下图:
这也就是说,在子进程执行AOF重写期间,服务器进程须要执行如下三个工做:
这样一来能够保证:
当子进程完成AOF重写工做以后,它会向父进程发送一个信号,父进程在接到该信号之 后,会调用一个信号处理函数,并执行如下工做:
这个信号处理函数执行完毕以后,父进程就能够继续像往常同样接受命令请求了。
在整个AOF后台重写过程当中,只有信号处理函数执行时会对服务器进程(父进程)形成 阻塞,在其余时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能形成的影 响降到了最低。
Redis 还能够同时使用 AOF 持久化和 RDB 持久化。 在这种状况下, 当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 由于 AOF 文件保存的数据集一般比 RDB 文件所保存的数据集更完整。可是 AOF 恢复比较慢,Redis 4.0 推出了混合持久化。
混合持久化: 将 rdb 文件的内容和增量的 AOF 日志文件存在一块儿。这里的 AOF 日志再也不是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,一般这部分 AOF 日志很小。
因而在 Redis 重启的时候,能够先加载 RDB 的内容,而后再重放增量 AOF 日志就能够彻底替代以前的 AOF 全量文件重放,重启效率所以大幅获得提高。
以为文章不错的话,小伙伴们麻烦点个赞、关个注、转个发一下呗~你的支持就是我写文章的动力。
更多精彩的文章请关注公众号“蘑菇睡不着”。
你越主动就会越主动,咱们下期见~