RDB持久化会直接保存某一时刻的数据快照,而AOF持久化是直接记录Redis执行过的命令来记录数据库状态。

AOF持久化的实现

AOF持久化功能可以分为命令追加,文件写入,文件同步这三个步骤。

命令追加

如果AOF功能处于开启状态,当Redis执行了一个写命令后,会将执行的写命令追加到aof_buf缓冲区末尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 这个redisServer保存了书中从前到后所展现过的所有结构
struct redisServer {
// 一个数组,保存着服务器中所有的数据库
redisDb *db;

// 服务器数据库的数量
int dbnum;

// 记录了保存条件的数组
struct saveparam *saveparam;

// 修改计数器
long long dirty;

// 上一次执行保存的时间
time_t lastsave;

// AOF缓冲区
sds aof_buf;
};

文件写入

Redis服务器进程是一个事件循环,这个循环中文件事件负责接受客户端请求,以及发送回复命令,还有时间事件等,每个事件负责处理一部分内容。

在处理文件事件时可能会执行一些写命令,就会有一部分内容被追加到aof_buf缓冲区中,所以循环中每次结束事件,都会调用flushAppendOnlyFile函数,考虑是否将缓冲区中的内容追加到AOF文件当中。伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
void loop() {
while (true) {
// 文件处理事件,有可能将新内容追加到aof_buf缓冲区
processFileEvents();

// 处理时间事件
processTimeEvents();

// 考虑是否将缓冲区内容保存到AOF文件
flushAppendOnlyFile();
}
}

flushAppendOnlyFile怎么执行由appendfsync来配置。具体如下:

always:将缓冲区所有内容写入并同步到AOF文件。

everysec:将缓冲区所有内容写入到AOF文件,如果上次上次同步时间距离这次超过1秒,那么再次对AOF文件进行同步。这个同步操作是由一个线程专门操作的。

no:将内容写入AOF,但不进行同步,什么时候同步由操作系统决定。

文件的写入与同步

现代的操作系统为了提升效率,在文件写入时会先把数据写入缓冲区,在缓冲区写满或者超出指定时间再去写入文件。这里的写入不进行同步指的是写入到操作系统设计的写入缓冲区,但是还没有真正的写入AOF文件,等到缓冲区写满就会被真正的写入AOF文件。

这三种策略的效率与安全性

always:因为每次都会写入并且同步,安全性最高,即使宕机也只会丢失一个时间循环中所修改的数据。但是同步过程牵扯到写磁盘操作,所以效率比较低。

everysec:每隔一秒会同步一次,效率可以接受,安全性只有可能丢失1s的数据。

no:效率最高,因为不进行同步,如果宕机则会丢上次同步到当前所修改的所有数据。

AOF重写

AOF重写用于缩小AOF文件,考虑一种场景,redis存了一个key为msg,值为hello world的键值对,然后对其做了数次修改,最终又回到了hello world,但是AOF文件却记录了很多条命令,这些命令其实是多余的。AOF重写就是为了去掉这些多余的命令。

重写的实现

AOF重写是用一个新的AOF文件来代替旧的AOF文件。生成新的AOF文件并不需要对旧的AOF文件进行读取,而是根据当前数据库数据生成的。比如list集合中有2个元素,然后经过多次操作最终有5个元素。其实可以直接根据这5条记录生成命令,然后写入新的AOF文件。

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void aof_rewrite(new_file_name) {
// 创建新的文件
f = create_file(new_file_name);

// 遍历数据库
for db in redisServer.db {
// 为空则跳过
if (db.is_empty) {
continue;
}

// 写入select 命令,用于加载时确定数据库
f.write_command("select" + db.id);

for key in db {
// 如果过期则忽略
if (key.is_expired()) {
continue;
}

// 根据键类型进行重写
if (key.type == String) {
rewrite_string(key);
} else if (key.type == List) {
rewrite_list(key);
} else if (key.type == Hash) {
rewrite_hash(key);
} else if (key.type == Set) {
rewrite_set(key);
} else if (key.type == SortedSet) {
rewrite_sorted_set(key);
}

// 如果键带有过期时间,则过期时间也要重写
if (key.have_expire_time()) {
rewrite_expire_time();
}
}

// 写入完毕
f.close();
}
}

AOF后台重写

因为AOF重写期间会执行大量的写入文件操作,所以如果用主进程去进行重写,则会造成阻塞。所以Redis将AOF重写放入子进程中。这样有两个好处,一是父进程可以继续处理请求,二是子进程带有服务器进程数据副本,使用子进程而不是子线程,可以在避免使用锁的情况下保证数据安全。

有一点需要注意,在执行AOF重写时,主进程还会处理请求,意味着已经写入的数据可能已经被修改。为了处理这个问题,Redis设置了一个AOF重写缓冲区,在创建子进程开始后,主进程处理的命令会同时写入AOF缓冲区AOF重写缓冲区

image-20230325130958729

在子进程重写完成后,会告知主进程,然后主进程会将AOF重写缓冲区的内容写入AOF缓冲区,这样就保证了数据一致。