0%

Redis中的持久化机制

Redis的数据全部存放在内存之中,如果一个服务器出现了宕机,那么内存中的数据将会全部丢失。为了解决这一个问题,需要用到Redis的持久化机制。Redis的持久化主要有两大机制,即AOF日志和RDB快照。

AOF日志

Redis提供了AOF日志来实现持久化的功能,AOF是通过记录保存到redis中的命令来记录数据库状态的。

AOF日志格式

在AOF日志中记录了Redis收到的每一条命令,这些命令使用文本形式进行保存。例如当Redis收到set testKey testValue 这样的命令之后,会在AOF日志中记录如下的内容:

1
2
3
4
5
6
7
*3
$3
set
$7
testKey
$9
testValue

其中*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
2
3
4
5
6
7
8
9
10
# 重写前
RPUSH list "A" "B"
RPUSH list "C"
RPUSH list "D" "E"
LPOP list
LPOP list
RPUSH list "F" "G"

# 重写后
RPUSH list "C" "D" "E" "F" "G"

在实际实现之中,因为避免在重写过程中造成客户端的缓冲区溢出,会检查键所包含的元素数量。如果元素的数量超过了一定的值,那么会使用多条命令记录键的值,而不是单单使用一条命令。

但是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
2
3
4
5
6
struct saveparam {
// 多少秒之内
time_t seconds;
// 发生多少次修改
int changes;
};

同时在服务器状态中还维护dirty计数器以及leavesave属性。dirty属性记录了距离上次RDB保存之后还执行了多少次修改,leavesave属性记录了上次保存的时间。在serverCron函数中会遍历saveparam,检查save的条件是否满足,一旦有一个条件满足,那么就会自动执行bgsave命令。

RDB与AOF混合使用

Redis中可以将了混合使用AOF和RDB的方法。RDB可以通过混合使用AOF日志来实现增量的更新。在两次RDB操作之间,使用AOF日志来记录这之间所有的命令操作。通过这样的方法,在完成之后清空AOF日志中的内容。通过这种方式,RDB不需要很频繁的执行,同时减少了AOF日志的文件大小。