RDB持久化
由于Redis的数据保存在内存中,所以一旦进程退出或者机器宕机,那么之前存储的那些数据都会丢失。因此,Redis推出了两种持久化机制,AOF和RDB。无论是哪一种持久化策略,都是以文件的形式被保存在硬盘上。
(RDB机制既可以手动触发,也会定期执行)
RDB文件的创建与导入
生成RDB文件的两个命令:SAVE 和 BGSAVE。
接下里,我们来看一下两者的区别。
SAVE 命令会阻塞Redis的工作线程(执行命令的线程),直到 RDB 文件创建完毕,在此期间,服务器不会响应任何命令。
BGSAVE 命令会派生出一个子进程用于创建和写入 RDB 文件,不会影响父进程,服务器可以继续响应客户端的请求。
对于RDB文件的载入,在Redis服务器启动的过程中,自动完成。不过,需要注意的是:如果服务器启用了AOF,那么优先加载 AOF 文件,反之,才会加载 RDB 文件还原数据库状态。
(AOF的写入频率更高,状态保留得更多,所以会优先加载)
SAVE 命令执行时,无法响应请求,这很好理解,因为工作线程被阻塞了。
BGSAVE命令在执行的过程中,无法响应 BGSAVE、SAVE、BGREWRITEAOF。
对于BGSAVE和SAVE而言,两个子进程写入文件会造成竞态条件。
对于BGREWRITEAOF而言,虽然是毫不相干的两个子进程,但是考虑到大量的I/O会影响性能,所以也不允许。
自动间隔性保存
Redis允许用户通过配置 save 选项,来定期执行RDB持久化。
(实质上是定期执行BGSAVE命令,可以思考一下为什么不是 SAVE 命令?)
save 选项中有很多的条件,满足其中一个即可触发RDB机制:
#
# Save the DB on disk:
#
# save <seconds> <changes>
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
#
# Note: you can disable saving completely by commenting out all "save" lines.
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save ""
save 900 1 // 900秒内至少执行了1次修改操作
save 300 10 // 300秒内至少执行了10次修改操作
save 60 10000 // 60秒内至少执行了10000次修改操作
dirty计数器和lastsave属性
struct redisServer {
// ...
long dirty; // 修改计数器
time_t lastsave; // 上一次保存的时间
// ...
}
dirty属性记录了距离上一次执行RDB持久化后,数据库状态的变化次数(对数据库进行了多少次的修改)。
lastsave属性记录了上一次RDB持久化的时间点。
Redis每隔100毫秒会执行一次serverCron函数,该函数用于数据库的维护操作,其中一个环节就是检查是否需要执行RDB持久化:依靠dirty和lastsave属性,判断是否与某一save条件匹配,如果匹配,那么就执行BGSAVE命令。
AOF持久化
与RDB持久化保存数据库的状态不同,AOF机制在AOF文件中保存的是所有的写命令。
AOF持久化的实现
1. 命令追加
struct redisServer {
// ...
sds aof_buf; // AOF缓冲区
// ...
}
当AOF持久化功能打开的时候,服务器在执行完一个命令后,会将该命令写入aof_buf缓冲区中。
2. AOF文件的写入与同步
Redis进程内部实际上是一个事件循环,每一次的循环会检查各类型的事件,执行对应的回调函数。可以理解为以下的伪代码:
while(true){
// ...
// 执行flushAppendOnlyFile函数
}
每次循环结束之前,都会执行flushAppendOnlyFile函数,该函数依赖配置文件中的appendFsync的值决定同步行为。
always => 立马同步
everysec => 如果当前时间距离上一次同步时间超过1秒钟,执行同步操作(一个独立的线程负责)
no => 由操作系统决定何时同步
所谓的同步操作,实际上就是将内存中的数刷入硬盘中。因为I/O操作需要操作系统介入,操作系统的默认策略是先将数据写到内核缓冲区中,到一定的时机(缓冲区满了、时间到了),再刷入硬盘中。这种策略虽然提高了效率,但是由于这种异步的写入方式,会有数据丢失的风险。因此,操作系统提供了fsync和fdatasync两个同步函数,可以强制让操作系统立即将缓冲区的数据刷入硬盘。
AOF文件的载入与数据的还原
因为AOF文件中保存的是所有的写命令,所以在服务器实例初始化的时候,将文件中的所有命令执行一遍即可。
服务器实例初始化的过程中,会创建一个【fake client】(假客户端)进程,不断的向redis服务器执行AOF文件中的命令,直到命令执行完毕。
AOF重写
如果redis开启了AOF持久化机制,随着时间的推移,AOF文件包含的命令会越来越多,体积也会越来越大(文件体积膨胀问题)。为了解决这个问题,就引入了AOF重写机制。
AOF文件重写的实现
就是用新的AOF文件替换掉老的AOF文件。
新的AOF中的命令从哪里来?
从数据库本身的数据来,然后生成对应的命令,这样操作下来,一个键有且仅有一个写命令了,达到了减少文件体积的作用。
AOF后台重写
很显然,如果在工作线程中进行AOF的重写操作,是一个不明智的选择。所以,该操作会在子进程中进行。
为什么不是子线程?
因为子进程可以携带父进程的数据副本,进程与进程之间互不干扰,避免了线程并发带来的安全性问题,以及加锁带来的性能开销。
当然,目前还有一个问题需要解决:在子进程进行重写的过程中,父进程很有可能又执行了很多的命令,此时就会造成数据的不一致。
为了解决这个问题,引入了一个AOF重写缓冲区。在执行AOF重写的过程中,会将写命令同时写入AOF缓冲区和AOF重写缓冲区中。子进程的重写任务执行完毕后,向父进程发送信号,父进程接受到信号后,会将AOF重写缓冲区中的内容写入新的AOF文件中去,然后“原子性的替换掉老的AOF文件。
(AOF重写的过程中,只有子进程的信号会阻塞父进程执行命令)
2024.10.25
writeBy kaiven