移步掘金:Redis–简述RDB与AOF持久化
0 引言
本篇博客是对“黄健宏先生-《Redis设计与实现》”一书中第二部分内容的梳理与总结,如果想要了解更多更详尽的内容,还请大家翻阅此书。
1 RDB持久化
RDB 持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个 RDB 文件中。
数据库状态:我们将服务器中的非空数据库及它们的键值对统称为数据库状态。
RDB 持久化功能所生成的 RDB 文件是一个经过压缩的二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态。
1.1 RDB文件的创建与载入
有两个命令可以用于生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE。
SAVE 命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕为止。与 SAVE 命令不同的是,BGSAVE 命令会派生一个子进程,由子进程负责创建 RDB 文件,服务器进程继续处理命令请求。
与 SAVE 命令和 BGSAVE 命令不同,RDB 文件的载入工作是在服务器启动时自动执行的,所以 Redis 并没有专门用于载入 RDB 文件的命令,只要 Redis 服务器在启动时检测到 RDB 文件的存在,就会自动载入 RDB 文件。
1.2 触发BGSAVE命令流程图
2 AOF持久化
与 RDB 持久化通过保存数据库中的键值对来记录数据库状态不同,AOF 持久化是通过保存 Redis 服务器所执行的写命令来记录数据库状态的。
AOF 持久化功能的实现可以分为命令追加、文件写入、文件同步三个步骤。
2.1 命令追加
当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令后,这个写命令会被追加到服务器状态的 aof_buf 缓冲区的末尾:
struct redisServer {
// ...
// AOF 缓冲区
sds aof_buf;
// ...
}
2.2 AOF文件的写入与同步
这里文件的写入是指将 aof_buf 中的内容写入至 os_buffer,同步是指将 os_buffer 中的内容同步至磁盘。因此同步才是真正意义上的将 aof_buf 中的数据落盘。
由于磁盘和内存的读写速度差了好几个数量级,因此现代操作系统为了提高文件的写入效率,提供了一个内存缓冲区(os_buffer)。在用户将一些数据真正要落盘之前,操作系统通常会将写入的数据暂时保存在 os_buffer 中,等缓冲区的空间被填满,或超过了指定的阈值,才会真正将 os_buffer 中的数据写入到磁盘中。
这种做法虽然提高了效率,但也带来了新的问题–数据一致性。如果计算机发生停机,那么保存在 os_buffer 中的写入数据将会丢失。为此,系统提供了 fsync 和 fdatasync 两个同步函数,可以强制让操作系统立即将 os_buffer 中的数据写入到磁盘中,从而确保写入数据的安全性。
Redis 服务器进程就是一个事件循环,这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复。
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到 aof_buf 缓冲区里面,所以服务器在每次结束一个事件循环之前,都会调用 flushAppendOnlyFile 函数,考虑是否要将 aof_buf 中的内容写入和保存到 AOF 文件里面。
flushAppendOnlyFile 函数的行为由 appendfsync 选项的值决定,各个不同值的行为如下:
appendfsync选项的值 | flushAppendOnlyFile函数的行为 | 效率 | 安全性 |
---|---|---|---|
always | 每次结束一个事件循环之前,都会将 aof_buf 缓冲区中的所有内容写入到 os_buffer,并同步到 AOF 文件中 | 最慢 | 最安全,即使出现故障停机,也只会丢失一个事件循环中所产生的命令数据 |
everysec | 将 aof_buf 中的所有内容写入到 os_buffer,如果上次同步 os_buffer 中的内容至 AOF 文件的时间距离现在超过 1s,那么再次对 AOF 文件进行同步。这个同步操作由另一个线程负责执行 | 足够快 | 故障停机时会丢失上次同步后距离现在未同步的所有数据(理论上不会丢失超过 1s 的数据) |
no | 将 aof_buf 中的内容写入至 os_buffer,但不进行同步,何时同步由操作系统决定 | 最快 | 由于数据同步的时间完全交由操作系统决定,因此该模式会在 os_buffer 中积累一段时间的写入数据,所以该模式的单次同步时长通常最长。从平摊操作的角度讲,该模式与 everysec 模式的效率类似。当出现故障停机时,会丢失上次同步 AOF 文件后的所有写命令 |
2.3 AOF重写
因为 AOF 持久化是通过保存被执行的写命令来记录数据库状态的,因此随着服务器运行时间的流逝,AOF 文件的内容会越来越多,如果不加以控制的话,体积过大的 AOF 文件很可能对 Redis 服务器、甚至整个宿主机造成影响,并且 AOF 文件体积越大,使用 AOF 进行数据还原所需的时间就越多。最后,从数据加载方面来说,我们并不需要历史状态,AOF 只需记录最新的状态,也就是能产生一致状态的命令即可。
为了解决 AOF 文件体积膨胀的问题,Redis 提供了 AOF 文件重写功能。通过该功能,Redis 服务器可以创建一个新的 AOF 文件来替代现有的 AOF 文件,新旧两个 AOF 文件所保存的数据库状态相同,但新 AOF 文件不会包含任何浪费空间的冗余命令。
2.3.1 AOF重写的实现
AOF 文件重写并不需要对现有的 AOF 文件进行任何读取,分析,或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。
考虑如下情况,服务器对 list 键执行了以下命令:
redis> RPUSH list "A" "B" // ["A", "B"]
(integer) 2
redis> RPUSH list "C" // ["A", "B", "C"]
(integer) 3
redis> RPUSH list "D" "E" // ["A", "B", "C", "D", "E"]
(integer) 5
redis> LPOP list // ["B", "C", "D", "E"]
"A"
redis> LPOP list // ["C", "D", "E"]
"B"
redis> RPUSH list "F" "G" // ["C", "D", "E", "F", "G"]
(integer) 5
那么服务器为了保存当前 list 键的状态,必须在 AOF 文件中写入 6 条命令。
如果服务器想用尽量少的命令来记录 list 键的状态,那么最简单高效的方法不是去读取和分析现有的 AOF 文件,而是直接从数据库中读取 list 键,然后用一条 RPUSH list "C" "D" "E" "F" "G"
命令来代替保存在 AOF 文件中的 6 条命令,这样就可以将保存 list 键所需的命令从 6 条减少为一条了。
如上,AOF 重写的实现原理就是从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是 AOF 重写功能的实现原理。
2.3.2 AOF后台重写
由于 Redis 服务器使用单个线程处理命令请求,而 AOF 重写会进行大量的写操作,所以如果由服务器直接调用 AOF 重写函数,那么 Redis 服务器线程将长时间被阻塞,在重写 AOF 期间,将无法处理客户端发来的请求。
Redis 不会因为 AOF 重写造成服务器无法处理请求,所以 AOF 重写被放到了子进程里执行,这样做可以同时达到两个目的:
- 子进程进行 AOF 重写期间,服务器进程可以继续处理命令请求
- 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以避免在使用锁的情况下,保证数据的安全性
不过在使用子进程进行 AOF 重写期间,服务器进程还会继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的 AOF 文件所保存的数据库状态不一致。
为了解决这种数据不一致的问题,Redis 服务器设置了 AOF 重写缓冲区,这个缓冲区在创建子进程后开始使用,当 Redis 执行完一个写命令后,他会同时将这个写命令发送给 AOF 缓冲区和 AOF 重写缓冲区,如下图:
这样一来可以保证:
- 引入 AOF 重写缓冲区,对现有 AOF 文件的处理不会造成任何影响
- 从创建子进程开始,服务器执行的所有写命令都会被记录到 AOF 重写缓冲区里
当子进程完成 AOF 重写后,他会向父进程发送一个信号,接收到信号的父进程会调用一个信号处理函数,停止执行客户端发来的命令请求,并执行以下工作:
- 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件所保存的数据库状态将和服务器当前的数据库状态一致
- 对新的 AOF 文件改名,原子的覆盖现有的 AOF 文件,完成新旧两个 AOF 文件的替换
在这个信号处理函数执行完毕后,父进程就可以继续接收客户端的命令请求了。
3 参考阅读
- 《Redis设计与实现》第二版,第二部分–黄健宏