Redis 的数据全部存放在内存之中,如果一个服务器出现了宕机,那么内存中的数据将会全部丢失。为了解决这一个问题,需要用到 Redis 的持久化机制。Redis 的持久化主要有两大机制,即 AOF 日志和 RDB 快照。
AOF 日志
Redis 提供了 AOF 日志来实现持久化的功能,AOF 是通过记录保存到 redis 中的命令来记录数据库状态的。
AOF 日志格式
在 AOF 日志中记录了 Redis 收到的每一条命令,这些命令使用文本形式进行保存。例如当 Redis 收到 set testKey testValue 这样的命令之后,会在 AOF 日志中记录如下的内容:
1 | *3 |
其中 * 3 代表当前命令中有三个部分,每个部分以 $ 开头,后面的数字代表该部分命令有多少字节,下一行代表该部分具体的命令。
Redis 在向 AOF 里面记录日志的时候,不会先去对这些命令进行语法检查,因此 Redis 采用了写后才记录日志的方式,避免记录了错误的日志被记录下来。
AOF 日志的实现
AOF 日志的实现可分为命令追加,文件写入以及文件同步。
命令追加
在服务器状态 redisServer 结构体中,有 aof_buf 字段。当需要记录 AOF 日志的时候,会将被执行的命令追加到服务器状态中的 aof_buf 缓冲区的末尾。
文件写入与同步
在 redis 结束每一个时间循环之前,会调用 flushAppendOnlyFile 函数,判断是否需要将 aof_buf 缓冲区中的保存到 AOF 文件之中。Redis 为 AOF 日志提供了三种写回策略,可以通过 appendfsync 选项来设置,其值分别为 Always,No,Everysec。
其中 Always 策略可以做到基本不丢数据,但是在每一个写命令后都必须保存到磁盘上,会导致性能的降低。
No 的写回时机由操作系统控制,在写到缓冲区之后,就可以继续执行后续的命令。但是何时保存到磁盘的时间是不确定的,一旦出现宕机,那么可能会有大量的数据丢失。
Everysec 采用每秒钟写一次的策略,在 Always 和 No 策略之间进行了折中。这也是 Redis 默认的 AOF 策略。
配置项 | 写回时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步写回 | 可靠性高,数据基本不会丢失 | 性能影响大 |
No | 操作系统控制的写回 | 性能高 | 丢失数据多 |
Everysec | 每秒写回 | 性能适中 | 会丢失 1 秒内的数据 |
AOF 重写
随着 AOF 越来越大,会出现 AOF 文件过大而导致的问题。例如操作系统无法保存过大的文件,通过在追加记录的时候也会导致速率变慢。如果出现宕机,会导致通过 AOF 日志恢复过程十分缓慢。为了解决这些问题,就需要使用到 AOF 的重写机制。
AOF 重写机制就是在重写时,将根据数据库的现状来创建一个新的 AOF 日志。重写机制将旧日志文件中的多条命令,在重写后的新日志变成了一条新的命令。例如以下六条命令,在经历 AOF 重写之后变成了一条指令。
1 | # 重写前 |
在实际实现之中,因为避免在重写过程中造成客户端的缓冲区溢出,会检查键所包含的元素数量。如果元素的数量超过了一定的值,那么会使用多条命令记录键的值,而不是单单使用一条命令。
但是 AOF 重写是一个非常耗时的操作,为了避免阻塞主线程,AOF 重写需要由子进程来完成的。但是在 AOF 重写的过程中,也需要继续处理命令请求,而新的命令请求可能导致当前数据库状态与重写后的数据库状态不一致。
为了解决这一问题,Redis 设置了一个 AOF 重写缓冲区。在 Redis 处理玩命令以后,会将这些命令同时发送到 AOF 缓冲区和 AOF 重写缓冲区。
在子进程完成 AOF 重写工作以后,会向父进程发送一个信号。父进程会在收到该信号以后,将重写缓冲区中的内容写到新的 AOF 文件之中,同时原子的对新的 AOF 文件进行改名,完成新旧文件的替换。
RDB 持久化
单独使用 AOF 日志在进行故障恢复的时候需要逐一将操作日志执行一遍。如果恢复的量比较大,会导致恢复速度缓慢,此时可以使用 RDB 内存快照的机制。RDB 记录的是某一时刻的数据而不是像 AOF 日志一样记录操作,在进行数据恢复的时候,可以直接将 RDB 的数据导入 Redis 中,很快速的完成数据的恢复。
RDB 文件结构
REDIS 字符
一个 RDB 文件最开头是 REDIS 五个字符,在载入 RDB 文件的时候,可以通过这五个字符来快速的判定是否为 RDB 文件。
DB_VERSION
在 REDIS 字符之后是一个 db_version 字段,长度为 4 字节,记录了 RDB 文件的版本号。
DATABASES
之后是 databases 部分,包含着任意个数据库中的键值对数据。如果服务器的数据库状态都为空,那么这个部分也为空。同时每一个数据库的保存内容也可以被分为 SELECTDB,db_number,key_value_pairs 三个部分。
SELECTDB 是一个常量,当读入这个值说明接下来要读入的是一个数据库号码。
db_number 保存着一个数据库号码,根据号码的不同,长度可以为 1,2,5 字节。当读到 db_number 字段的时候,会进行数据库切换。
key_value_pairs 部分保存着数据库中所有键值对数据。其由 EXPIRETIME_MS,ms,TYPE,key,value 五个部分组成。其中 EXPIRETIME_MS 和 ms 仅仅在带有过期时间的键值对中出现。
EXPIRETIME_MS 是一个常量字段,告诉读入程序接下来是一个过期时间字段。
ms 是一个 8 字节的整数,以毫秒为单位的 UNIX 时间戳,记录着键值对的过期时间。
Type 字段记录了 value 的类型,长度为 1 个字节,代表这当前 value 数据的类型。
key 是一个字符串对象,总是以 REDIS_RDB_TYPE_STRING 的形式保存。
value 根据 TYPE 的值不同,Redis 会根据不同的类型来进行不同方式的保存。
EOF
EOF 字段代表着 RDB 文件的正文正式结束。当 REDIS 读入 RDB 文件的时候,如果读到该字段,说明所有数据库的键值对已经载入完毕了。
CHECK_SUM
最后是一个部分是 check_sum,通过对前面四个部分通过 CRC64 算法计算得出。在载入 RDB 文件的时候,服务器会通过该字段来判断是否出现了文件损坏。
RDB 实现
Redis 提供了 save 和 bgsave 命令来生成 RDB 文件,其中 save 命令在主线程执行 RDB 的操作,会阻塞主线程的执行,而 bgsave 会创建一个子进程,专门用于写入新的 RDB 文件,避免主线程的阻塞。
在执行 RDB 记录的时候,Redis 会使用操作系统提供的写时复制技术(COW)来实现在生成 RDB 文件的同时可以正常的处理请求。如果主线程对 Redis 中的数据执行的是读操作,那么主线程与子线程互不影响。如果主线程时写操作,那么该数据会被复制一份,生成该数据的副本,提供给子线程,子线程会把这个副本的数据写入到 RDB 文件之中,而在这个过程中,主线程仍然可以修改原来的数据。这一机制既保证了数据的完整性,同时允许主线程进行数据修改。
RDB 自动保存
在 Redis 服务器启动的时候,可以通过传递配置文件等方式设置 save 选项。根据设置的条件,Redis 会设置服务器状态中的 saveParams 属性,其类型为 saveparam 的数组。saveparam 结构体有两个属性,表示了在多少秒内发生了多少次修改会触发 RDB 保存,saveparam 的源码如下:
1 | struct saveparam { |
同时在服务器状态中还维护 dirty 计数器以及 leavesave 属性。dirty 属性记录了距离上次 RDB 保存之后还执行了多少次修改,leavesave 属性记录了上次保存的时间。在 serverCron 函数中会遍历 saveparam,检查 save 的条件是否满足,一旦有一个条件满足,那么就会自动执行 bgsave 命令。
RDB 与 AOF 混合使用
Redis 中可以将了混合使用 AOF 和 RDB 的方法。RDB 可以通过混合使用 AOF 日志来实现增量的更新。在两次 RDB 操作之间,使用 AOF 日志来记录这之间所有的命令操作。通过这样的方法,在完成之后清空 AOF 日志中的内容。通过这种方式,RDB 不需要很频繁的执行,同时减少了 AOF 日志的文件大小。