一、RDB持久化:
1. RDB持久化的作用:
Redis数据库中的数据都是保存在内存中的,为了避免服务器进程退出 或者 出现主机下电导致的数据丢失,服务器需要定时的将数据从 内存 存储到 磁盘 中以达到 持久化 的目的。
并且服务器还可以从磁盘中保存的RDB文件中还原数据库的状态。
RDB持久化 既可以手动执行,也可以根据服务器配置选项 定期执行。
总结起来一句话,Redis的RDB持久化功能就是为了避免保存在内存中的数据丢失。
2. RDB文件的创建和载入:
2.1 创建:
有两个 Redis命令 用于 生成RDB文件: SAVE
、BGSAVE
.
redis> SAVE
OK
redis> BGSAVE
Background saving started
区别在于:
(1)SAVE命令 会阻塞 Redis服务器进程,直到 RDB文件 创建完毕,在此期间服务器的进程阻塞,服务器不能处理任何命令请求;
(2)BGSAVE命令 不会阻塞服务器进程,而是 fork 出一个子进程,由 子进程 来负责 创建RDB文件,父进程可以继续正常处理命令请求。在 子进程完成RDB创建后,会发送信号通知父进程。
通过伪代码可以看出二者区别:
SAVE() {
rdbSave(); //创建RDB文件
}
BGSAVE () {
pid = fork(); //创建子进程
if (pid == 0) {
rdbSave();
signal_parent(); //子进程负责创建RDB文件,完成后发送信号通知父进程
}
else if (pid>0) {
handle_request_and_wait_signal(); //父进程继续处理命令请求,并轮询等待子进程的信号
}
else {
handle_fork_error(); //fork()返回负数,创建子进程出错
}
}
2.2 载入:
Redis在启动阶段会自动检测是否存在RDB文件,如果存在则自动载入。
在此之前,Redis会先检测是否存在AOF文件,因为AOF文件的更新频率比RDB文件的更新频率更高, 所以如果Redis打开了AOF持久化功能,则优先载入AOF文件。
3. 自动间隔性保存:
3.1 设置 saveparam 保存条件:
Redis允许用户通过设置服务器配置文件中的 save选项 (redis.conf),让服务器每隔一段时间自动执行一次 BGSAVE命令 在后台生成RDB文件。
redis.conf 配置文件中 的 默认配置 如下:
save 900 1 //在900秒之内,对数据库进行至少1次修改
save 300 10 //在300秒之内,对数据库进行至少10次修改
save 60 10000 //在60秒之内, 对数据库进行至少10000次修改
表示 只要满足上面三个条件中的一个,BGSAVE 就会执行。
这段逻辑读起来有点绕,总的思想就是避免 BGSAVE 执行的太过频繁或执行的不及时。
3.2 服务器是如何实现通过save条件进行自动保存的:(在serverCron中执行)
在 redisSever->saveparams 结构体数组中 保存配置文件中的 save选项:
struct redisServer {
struct saveparam *saveparams; //记录保存条件的数组
int saveparamslen; //saveparams数组的元素个数
long long dirty; //修改计数器。记录距离上一次成功执行SAVE或BGSAVE后,服务器对数据库状态进行了多少次修改(包括写入、删除、更新等操作
time_t lastsave; //上一次执行保存的时间
}
struct saveparam {
time_t seconds; //秒数
int changes; //修改数
};
在 serverCron 函数中检查保存条件是否满足:
int serverCron() {
for(j = 0; j < server.saveparamslen; j++) {
saveparam = server.saveparams[j];
save_interval = unix_time.now() - server.lastsave; //距离上次执行BGSAVE的间隔时间
if(server.dirty >= saveparam.changes && saveparam.seconds) {
BGSAVE(); //&&
}
}
}
4. RDB文件的数据结构:
RDB的总体文件结构:
带有两个非空数据库的RDB文件示例:
RDB文件中的数据库结构:
数据库结构示例:
RDB文件中的数据库结构示例:
二、AOF持久化:
5. AOF持久化:
AOF = Append Only File 。
与RDB持久化通过保存数据库中的 键值对 来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的 写明令 来记录数据库状态的。
6. AOF持久化的实现:
AOF持久化功能的实现分为 命令追加(append)、文件写入、文件同步(sync) 三个步骤。
6.1 命令追加:
当AOF持久化功能打开时,服务器每执行完一个写命令后,就会以 协议格式 将被执行的 写明令 追加到服务器的 aof_buf缓冲区 的末尾:
typedef char* sds;
struct redisSever {
sds aof_buf; //AOF缓冲区
};
例如,客户端向服务器发送命令:
redis> SET KEY VALUE
OK
服务器在执行 SET命令 后,会按照 协议格式 将其追加到 aof_buf缓冲区 末尾:
*3\r\n$3\r\nSET\I\n$3\r\nKEY\r\n$5\r \nVALUE\r\n
6.2 文件写入与同步:
6.2.1 操作系统对于文件的写入与同步的处理:
为了提高文件的写入效率,在现在操作系统中,当用户调用 write函数,将一些数据写入到文件时,操作系统通常会将写入数据暂存在一个 内存缓冲区 中,等待缓冲区满 或 超过限定值时,才会真正的将缓冲区中的数据写入磁盘,即 写入 与 同步 是两个分离的动作。
这种做法的好处是提高了效率,缺点是不够安全,如果计算机宕机,则保存在内存缓冲区中的数据将会丢失。
为此,操作系统提供了两个同步函数: fsync 和 fdatasync,用于强制将内存缓冲区中的数据立即写入磁盘。
6.2.2 AOF文件的写入与同步:
在Redis的主循环中,每次处理完 I0事件 和 定时器事件 后,会调用一次 flushAppend0nlyFile
函数,判断是否需要将aof_buf缓冲区 中的数据写入到 AOF文件中。
void aeMain(aeEventLoop *eventLoop)[
while(!eventLoop->stop) {
aeProcessEvents(eventLoop, AE_ALL_EVENTS); //在Redis的主循环中
}
}
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
numevents = aeApiPoll(eventLoop); //处理IO事件
for(j = 0; j < numevents; j++) {
if(mask & AE_READABLE) fe->rfileProc();
if(mask & AE_WRITABLE) fe->wfileProc();
}
processTimeEvents(eventLoop); //处理定时器事件,在其中调用serverCron函数
}
int serverCron(eventLoop) {
//先判断是否需要执行 RDB BGSAVE(RDB持久化的执行频率会小于 AOF持久化)
for(j = 0; j < server.saveparamslen; j++) {
rdbSaveBackGround();
}
if(server.aof_flush_postponed_start) {
flushAppendOnlyFile(0); //将aof_buf缓冲区中的内容写入到AOF文件
}
}
与操作提供对write函数的处理类似,flushAppendOnlyFile 函数的 写入 与 同步 也是分离的两步,每次调用 flushAppendOnlyflle函数 时,都会将 aof_buf缓冲区 中的数据写入到 AOF文件(此时AOF
文件仍在内存中),至于是否需要将 AOF文件 从 内存 写入 磁盘(“同步”),则由 redis.conf 配置文件 中的 appendfync
配置项决定:
# appendfsync always //将AOF文件立即写入磁盘
appendfsync everysec //如果距离上次同步AOF文件的时间已经超过一秒钟,则再次对AOF文件同步,并且由一个线程专门执行
# appendf sync no //从不对AOF文件主动执行同步,何时同步由操作系统决定
其中,always方式的安全性最高,但效率最低; no方式的效率最高,但安全性最差;everysec方式折中考虑,每隔一秒钟对AOF文件同步一次,既兼顾了效率,如果服务器突然宕机也只是丢失一秒钟的写入数据,损失可控。
evenysec方式是Redis的默认同步策略。
6.3 AOF文件的载入与数据还原:
Redis在读取AOF文件还原数据库状态时,会创建一个不带网络连接的伪客户端(因为Redis的命令只能在客户端中执行),从AOF文件中逐条读取写明令并执行,直至将AOF文件中的所有命令处理完毕。
6.4 AOF文件的重写:
随着服务器运行时间的拉长,执行的写明令会越来越多,AOF文件的体积也会越来越大,为了避免过大的AOF文件对服务器造成影响,需要对AOF文件进行重写。
虽然这个操作名为“AOF重写”,但实际上它不会对原有的AOF文件进行任何 读取、分析、写入 的操作,而是通过分析当前数据库的状态来另外生成一个AOF文件,直接读取当前数据库中键值对的值,然后执行对应的写操作生成AOF文件。(相当于将重复冗余的读写命令进行删除合并)
AOF文件的重写操作由 aof_rewrite函数 执行,由于Redis使用单线程处理网络I0事件,而 aof_rewrite函数会进行大量的写入操作,所以为了避免执行AOF重写期间导致线程阻塞无法处理客户端请求,Redis会单独 fork 出一个子进程专门处理AOF重写。
使用子进程而不是子线程的原因是:
子进程带有服务器进程的数据副本,可以在避免使用锁的情况下,保证数据的安全性。
然而,使用子进程同样引入一个问题,当子进程在进行AOF重写时,服务器主进程还在继续处理客户端的命令请求,而新的命令请求可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。
为了解决数据不一致的问题,Redis设置了一个“AOF重写缓冲区”,用于保存在子进程进行AOF重写期间的客户端的写命令请求。 当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在收到信号后会调用一个 信号处理函数,执行以下工作:
(1)将 “AOF重写缓冲区” 中的所有内容写入到 新的 AOF文件中(这时 新的AOF文件所保存的数据库状态与服务器的当前状态一致);
(2)对新的AOF文件进行改名,原子的覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
在整个AOF后台重写过程中,只有调用信号处理函数时会造成服务器进程 阻塞,其他时间服务器都可以正常处理客户端的命令请求,这将AOF重写对服务器的性能影响降到了最低。