Redis持久化操作--原理剖析

RDB持久化

RDB概述

Redis是一个键值对数据库服务器吗,服务器中通常包含着任意个非空数据库。

数据库状态示例

因为Redis是内存数据库,它将自己的数据库状态储存在内存里面,所以如果不想办法将储存在内存中的数据库状态保存到磁盘里面,那么一旦服务器进程退出,服务器中的数据库状态也会消失不见。

为了解决这个问题,Redis提供了RDB持久化功能,这个功能可以将Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失。

RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。

         因为RDB文件是保存在硬盘里面的,所以即使Redis服务器进程退出,甚至运行Redis服务器的计算机停机,但只要RDB文件仍然存在,Redis服务器就可以用它来还原数据库状态。

RDB文件的创建与载入

创建:

        SAVE 命令会阻塞Redis服务器进程,直到 RDB 文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。客户端发送的所有命令请求都会被拒绝,只有在服务器执行完SAVE命令、重新开始接受命令请求之后,客户端发送的命令才会被处理。

        而 BGSAVE 命令会派生出一个子进程,然后由子进程负责创建 RDB 文件,服务器进程(父进程)继续处理命令请求。在BGSAVE命令执行期间,服务器处理SAVE、BGSAVE、BGREWRITEAOF三个命令的方式会和平时有所不同。

        首先,在BGSAVE命令执行期间,客户端发送的SAVE命令会被服务器拒绝,服务器禁止SAVE命令和BGSAVE命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个rdbSave调用,防止产生竞争条件。

        其次,在BGSAVE命令执行期间,客户端发送的BGSAVE命令会被服务器拒绝,因为同时执行两个BGSAVE命令也会产生竞争条件。 

载入:

        和使用 SAVE 命令或者 BGSAVE 命令创建 RDB 文件不同,RDB文件的载人工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件。 

        服务器在载人RDB文件期间,会一直处于阻塞状态,直到载人工作完成为止。

另外值得一提的是,因为 AOF 文件的更新频率通常比 RDB 文件的更新频率高,所以:

1. 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。

2. 只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。服务器判断该用哪个文件来还原数据库状态的流程如图所示。

载入 RDB 文件的实际工作由 rdb.c/rdbLoad 函数完成,这个函数和 rdbSave 函数之间的关系可以用下图表示。

 RDB文件结构

RDB 文件结构

         RDB文件的最开头是REDIS部分,这个部分的长度为5字节,保存着"REDIS"五个字符。通过这五个字符,程序可以在载人文件时,快速检查所载入的文件是否RDB文件。

        db_version长度为4字节,它的值是一个字符串表示的整数,这个整数记录了RDB文件的版本号,比如 "0006" 就代表RDB文件的版本为第六版。

         databases 部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据:

1. 如果服务器的数据库状态为空(所有数据库都是空的),那么这个部分也为空,长度为0字节。
2. 如果服务器的数据库状态为非空(有至少一个数据库非空),那么这个部分也为非空,根据数据库所保存键值对的数量、类型和内容不同,这个部分的长度也会有所不同。

       EOF 常量的长度为1字节,这个常量标志着 RDB 文件正文内容的结束,当读入程序遇到这个值的时候,它知道所有数据库的所有键值对都已经载入完毕了。 

        check_sum是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对REDIS、db_version,databases、EOF 四个部分的内容进行计算得出的。服务器在载人RDB文件时,会将载人数据所计算出的校验和与check sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。 

作为例子,下图展示了一个 databases 部分为空的 RDB 文件:文件开头的 "REDIS" 表示这是一个 RDB 文件,之后的 "0006" 表示这是第六版的 RDB 文件,因为databases为空,所以版本号之后直接跟着 EOF 常量,最后的 6265312314761917404 是文件的校验和。

databases 部分为空的RDB 文件

 databases部分:

一个 RDB 文件的 databases 部分可以保存任意多个非空数据库。

        例如,如果服务器的0号数据库和3号数据库非空,那么服务器将创建一个如图所示的RDB文件,图中的database 0代表0号数据库中的所有键值对数据,而database 3则代表3号数据库中的所有键值对数据。

带有两个非空数据库的RDB文件示例

         每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分,如图所示。

RDB 文件中的数据库结构

 SELECTDB 常量的长度为1字节,当读入程序遇到这个值的时候,它知道接下来要读入的将是一个数据库号码。

db_number 保存着一个数据库号码,根据号码的大小不同,这个部分的长度可以是1字节、2字节或者5字节。当程序读人db number部分之后,服务器会调用SELECT命令,根据读入的数据库号码进行数据库切换,使得之后读人的键值对可以载入到正确的数据库中。

key_value_pairs 部分保存了数据库中的所有键值对数据,如果键值对带有过期时间,那么过期时间也会和键值对保存在一起。根据键值对的数量、类型、内容以及是否有过期时间等条件的不同, key value pairs部分的长度也会有所不同。

作为例子,下图展示了RDB文件中,0号数据库的结构。

数据库结构示例

 另外,下图则展示了一个完整的RDB文件,文件中包含了0号数据库和3号数据库。

RDB文件中的数据库结构示例

 key_value_pairs 部分:

        RDB文件中的每个key_value_pairs部分都保存了一个或以上数量的键值对,如果键值对带有过期时间的话,那么键值对的过期时间也会被保存在内。不带过期时间的键值对在RDB文件中由TYPE、key、value三部分组成,如图所示。

不带过期时间的键值对

 TYPE 记录了value的类型,长度为1字节,值可以是以下常量的其中一个:

 以上列出的每个TYPE常量都代表了一种对象类型或者底层编码,当服务器读入RDB文件中的键值对数据时,程序会根据TYPE的值来决定如何读人和解释value的数据。key 和value分别保存了键值对的键对象和值对象:

1. 其中key总是一个字符串对象,它的编码方式和REDIS_RDB_TYPE_STRING类型的value一样。根据内容长度的不同, key的长度也会有所不同。
2. 根据TYPE类型的不同,以及保存内容长度的不同,保存value的结构和长度也会有所不同。

        带有过期时间的键值对在RDB文件中的结构如图所示。

带有过期时间的键值对

带有过期时间的键值对中的TYPE、key、value三个部分的意义,和前面介绍的不带过期时间的键值对的TYPE、key、value三个部分的意义完全相同,至于新增的EXPIRETIME MS和ms,它们的意义如下: 

1. EXPIRETIME_MS 常量的长度为1字节,它告知读入程序,接下来要读人的将是一个以毫秒为单位的过期时间。
2. ms是一个8字节长的带符号整数,记录着一个以毫秒为单位的UNIX时间截,这个时间截就是键值对的过期时间。

作为例子,下图展示了一个没有过期时间的字符串键值对。

无过期时间的字符串键值对示例 

 下图展示了一个带有过期时间的集合键值对,其中键的过期时间为1388556000000(2014年1月1日零时)。

带有过期时间的集合键值对示例

 value的编码:

        RDB文件中的每个value部分都保存了一个值对象,每个值对象的类型都由与之对应的TYPE记录,根据类型的不同, value部分的结构、长度也会有所不同。

AOF持久化

        除了RDB持久化功能之外, Redis还提供了AOF (Append Only File)持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,如图所示。

AOF 持久化

 AOF 持久化的实现

        AOF持久化功能的实现可以分为命令追加(append)文件写人文件同步(sync)三个步骤。

文件的写入和同步

        为了提高文件的写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写入到磁盘里面。

        这种做法虽然提高了效率,但也为写入数据带来了安全问题,因为如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失。

        为此,系统提供了fsync和fdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性。 

 AOF 持久化的效率和安全性

        服务器配置 appendfsync 选项的值直接决定 AOF 持久化功能的效率和安全性。

        当 appendfsync 的值为 always 时,服务器在每个事件循环都要将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,并且同步 AOF 文件,所以 always 的效率是 appendfsync 选项三个值当中最慢的一个,但从安全性来说,always也是最安全的,因为即使出现故障停机,AOF持久化也只会丢失一个事件循环中所产生的命令数据。

        当 appendfsync 的值为 everysec 时,服务器在每个事件循环都要将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,并且每隔一秒就要在子线程中对 AOF 文件进行一次同步。从效率上来讲, everysec 模式足够快,并且就算出现故障停机,数据库也只丢失一秒钟的命令数据。

        当 appendfsync 的值为 no 时,服务器在每个事件循环都要将 aof buf 缓冲区中的所有内容写入到 AOF 文件,至于何时对 AOF 文件进行同步,则由操作系统控制。因为处于 no 模式下的 flushAppendonlyFile 调用无须执行同步操作,所以该模式下的 AOF 文件写入速度总是最快的,不过因为这种模式会在系统缓存中积累一段时间的写入数据,所以该模式的单次同步时长通常是三种模式中时间最长的。从平摊操作的角度来看,no 模式和 everysec 模式的效率类似,当出现故障停机时,使用 no 模式的服务器将丢失上次同步 AOF 文件之后的所有写命令数据。

AOF文件的载入与数据还原 

        因为 AOF 文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读人并重新执行一遍 AOF 文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。Redis 读取 AOF 文件并还原数据库状态的详细步骤如下:

AOF文件载人过程

 AOF重写

        因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积也会越来越大,如果不加以控制的话,体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。 

        为了解决AOF文件体积膨胀的问题, Redis提供了AOF文件重写(rewrite)功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。

AOF后台重写

        上面介绍的AOF重写程序aof_rewrite函数可以很好地完成创建一个新AOF文件的任务,但是,因为这个函数会进行大量的写人操作,所以调用这个函数的线程将被长时间阻塞,因为Redis服务器使用单个线程来处理命令请求,所以如果由服务器直接调用aof_rewrite 函数的话,那么在重写AOF文件期间,服务器将无法处理客户端发来的命令请求。

        很明显,作为一种辅助性的维护手段,Redis 不希望AOF重写造成服务器无法处理请求,所以Redis 决定将AOF文件重写程序放到子进程里执行,这样做可以同时达到两个目的:

1. 子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求
2. 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。

        不过,使用子进程也有一个问题需要解决,因为子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。 

        为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区,如图所示。 

服务器同时将命令发送给AOF文件和AOF重写缓冲区 

 这也就是说,在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:

  1. 执行客户端发来的命令。
  2. 将执行后的写命令追加到AOF缓冲区。
  3. 将执行后的写命令追加到AOF重写缓冲区。

这样一来可以保证:

  1. AOF 缓冲区的内容会定期被写人和同步到AOF文件,对现有AOF文件的处理工作会如常进行。
  2. 从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面。

这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了。在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。

重点回顾:

RDB持久化:

  1. RDB文件用于保存和还原Redis服务器所有数据库中的所有键值对数据。
  2. SAVE命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器。
  3. BGSAVE 令由子进程执行保存操作,所以该命令不会阻塞服务器。
  4. 服务器状态中会保存所有用save选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行BGSAVE命令。
  5. RDB文件是一个经过压缩的二进制文件,由多个部分组成。
  6. 对于不同类型的键值对,RDB文件会使用不同的方式来保存它们。

AOF持久化:

  1. AOF文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态。
  2. AOF文件中的所有命令都以Redis命令请求协议的格式保存。
  3. 命令请求会先保存到AOF缓冲区里面,之后再定期写人并同步到AOF文件。
  4. appendfsync选项的不同值对AOF持久化功能的安全性以及Redis服务器的性能有很大的影响。
  5. 服务器只要载入并重新执行保存在AOF文件中的命令,就可以还原数据库本来的状态。
  6. AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
  7. AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读人、分析或者写人操作。
  8. 在执行BGREWRITEAOF命令时, Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值