适合人群
有一定Redis基础,想对Redis的持久化做深入了解的。
前言
阅读本文可以深入理解Redis持久化,本章不会对持久化概念做过多介绍,而是深入实现原理。
定义
持久化 : 我们知道redis是内存数据库,所有的数据都存储在内存中,如果服务器意外宕机或者服务器主进程意外退出,这时我们的数据会丢失。redis为了解决这个问题,提供了持久化功能,来避免这种数据丢失。
一、RDB持久化
rdb持久化是生成一个二进制文件。
触发rdb持久化时机
1.手动执行。
通过save 命令和 bgsave 命令,两个命令的区别是 save 命令阻塞主进程而bgsave命令是通过fork()函数创建出来的子进程进行持久化,不阻塞主进程。无论是save命令还是bgsave命令,最终都是执行服务器的rdbSave()函数,下面会讲解save和bgsave命令都干了些什么事。
2. 通过服务器配置选项定期执行。
通过redis.conf配置文件的save配置来检测什么时候触发rdb持久化。
save 900 1 // 900秒内至少有1个key改变
save 300 10 // 300秒内至少有10个key改变
save 60 10000 // 60秒内至少有10000个key改变
下面是检测代码 :
// 进行rdb检测,如果关闭rdb,则saveparamslen为0。按照上面进行配置,第一个检测的是save 900 1,依次进行。
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
// 满足 【变化个数】&【持续时间】&【根据上次是否rdb成功,进行判断是否需要延迟】
if (server.dirty >= sp->changes
&& server.unixtime - server.lastsave > sp->seconds
&&(server.unixtime - server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY || server.lastbgsave_status == C_OK)) {
rdbSaveBackground(server.rdb_filename, NULL);
break;
}
}
dirty 属性含义 : 服务器对多少key进行了改变,每次rdb持久化之后都会重新计算。
lastsave 属性含义 : 上次进行rdb持久化的时间
通过代码可以看出,服务器改变了1个key,并且时间过去了900秒,那么执行rdbSaveBackground()函数。
// CONFIG_BGSAVE_RETRY_DELAY 默认值为 5
// 这段代码的含义是如果上次执行rdb成功,那么这次也可以执行rdb。如果上次执行rdb失败,必须等 5 秒后才可以再次执行rdb。
// 因为上次执行rdb可能是fork()子进程失败(因为空间不足)或者其他原因导致rdb失败。
// 通过这个限制来控制频率,防止失败后立马再次进行rdb。
server.unixtime - server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY || server.lastbgsave_status == C_OK)
这里留个问题 : dirty 属性每次服务器执行key的改变都会进行增加,这个值是在什么时候清空的,因为主线程和bgsave是同时进行的?
a. 猜测一 : 如果在执行rdbSaveBackground()函数同时清空dirty属性,那么rdbSaveBackground()函数执行失败如何恢复dirty属性。
b. 猜测二 : 在rdbSaveBackground()函数执行后,通过通知主线程来计算dirty这个属性该赋值为多少,dirty属性值不一定赋值为0,dirty属性值取决于在rdbSaveBackground()期间进行了多少key的改变。
提前透漏下,猜测二是正确的~.~。
save命令的执行过程
下面的代码对一些无用的代码进行了删减,不影响正确理解功能的实现
// save命令的实现最后会调用rdbSave()函数
void saveCommand(client *c) {
// rdb_child_pid 是进行rdb时fork()出来子进程的pid,如果未执行,值为-1
// 如果正在进行rdb,则什么都不做。否则调用rdbsave
if (server.rdb_child_pid != -1) {
return;
}
rdbSave(server.rdb_filename, NULL);
}
bgsave命令的执行过程
// bgsave命令
// 1. 会调用rdbSaveBackground()函数,进行fork(),产生子进程也会掉用rdbSave()函数。
// 2. 定时任务会不断检测,如果子进程执行完,wait3()函数会返回子进程的pid,进行回调处理。
void bgsaveCommand(client *c) {
// rdb_child_pid 是进行rdb时fork()出来子进程的pid,如果未执行,值为-1
// aof_child_pid 是进行aof重写时fork()出来子进程的pid,如果未执行,值为-1
// 如果正在进行rdb 或者 aof重写则什么都不做。否则调用rdbSaveBackground()函数进行rdb
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
return;
}
rdbSaveBackground(server.rdb_filename, NULL)
}
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
// 如果正在进行rdb 或者 aof重写则什么都不做。
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
// dirty_before_bgsave属性记录下执行rdbsave前dirty的值。
// 目的是在执行完rdbsave后,服务器会用dirty值 - dirty_before_bgsave值来更新dirty属性值。
// 因为这个差值就是rdbsave期间有多少key变化。证实了上面的猜测二。
server.dirty_before_bgsave = server.dirty;
// rdbsave的执行时间,用于save配置的检测,是否执行rdbSaveBackground()
server.lastbgsave_try = time(NULL);
// fork() 函数返回0的为子进程,大于1的为主进程,小于0为fork失败
if ((childpid = fork()) == 0) {
/* Child */
rdbSave(filename, rsi);
} else {
/* Parent */
if (childpid == -1) {
// fork()函数执行失败,会记录上次的rdb状态为失败,用于save配置的检测,是否执行rdbSaveBackground()
server.lastbgsave_status = C_ERR;
return C_ERR;
}
// 服务器rdb_child_pid属性用来记录正在执行的bgsave的子进程pid
server.rdb_child_pid = childpid;
return C_OK;
}
return C_OK;
}
/* 定时任务会执行此函数,此函数做了很多事情,这里为了代码整洁,删除了其它代码 */
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
/* 如果后台正在进行rdb或者aof_rewrite,则进行检测 */
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
// wait3()函数会返回子进程的pid
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
// rdb 回调处理函数
if (pid == server.rdb_child_pid) {
backgroundSaveDoneHandler(exitcode,bysignal);
// aof_write 回调处理函数
} else if (pid == server.aof_child_pid) {
backgroundRewriteDoneHandler(exitcode,bysignal);
}
}
}
}
/* rdb回调处理backgroundSaveDoneHandler()函数最终会掉用
backgroundSaveDoneHandlerDisk()函数进行数据初始化 */
void backgroundSaveDoneHandlerDisk(int exitcode, int bysignal) {
// 猜测二验证正确,重新复制dirty属性
server.dirty = server.dirty - server.dirty_before_bgsave;
// 用于检测save配置
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
// 未在进行bgsave
server.rdb_child_pid = -1;
}
// 扫库写二进制流文件
int rdbSave(char *filename, rdbSaveInfo *rsi) {
char tmpfile[256];
FILE *fp;
rio rdb;
snprintf(tmpfile, 256, "temp-%d.rdb", (int) getpid());
fp = fopen(tmpfile, "w");
// 扫描数据库,往临时rdb文件写入二进制数据
rdbSaveRio(&rdb, &error, RDB_SAVE_NONE, rsi);
// 临时rdb文件替换原有rdb文件
rename(tmpfile,filename);
// 只有save命令同步执行rdbSave()函数,下面的变量初始化才有用。
// 否则子进程执行没有任何意义,因为执行完rdb会对子进程进行回收。
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
return C_OK;
}
rdb总结 :
1.save命令是同步调用rdbsave()函数进行持久化的。
2.bgsave命令或者定时任务检测save配置持久化,都是通过调用rdbSaveBackground()函数,fork子进程进行异步处理。子进程处理完后,主进程的定时任务serverCron()处理函数会检测到,调用backgroundSaveDoneHandlerDisk()函数进行数据初始化。
3.bgsave schedule 命令如果正在执行aof文件重写,那么会在aof重写完成后进行rdb持久化操作,这个不断检测是在定时任务serverCron()函数中进行的。上面没有讲解,是因为不想代码太过于复杂,难以理解。
二、AOF持久化
aof持久化是将每个redis的写命令都保存在aof文件里,aof文件的所有命令都是以redis命令的请求协议(resp协议)格式保存的纯文本文件。aof持久化分为命令追加、文件写入、文件同步。
命令追加 : 每次redis成功执行写命令后,会把命令按照请求协议(resp协议)放入到aof缓冲区中。
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
sds buf = sdsempty();
buf = "生成resp协议的命令";
/* 加入到 aof 缓冲区 */
if (server.aof_state == AOF_ON)
server.aof_buf = sdscatlen(server.aof_buf, buf, sdslen(buf));
/* 正在进行 bgrewriteaof,则把命令加入到 aof 重写缓冲区 */
if (server.aof_child_pid != -1)
aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
sdsfree(buf);
}
文件写入 : 每次在执行命令前,会把aof缓冲区中的数据写到文件(操作系统会把这个数据保存到内存缓冲区)。
文件同步 : 根据appendfsync配置的值,判断什么时候把操作系统的内存缓冲区的文件数据同步到磁盘文件里。
/* 每次在执行命令后,都会执行该方法进行文件写入,并根据appendfsync配置来选择何时进行文件同步 */
void flushAppendOnlyFile(int force) {
ssize_t nwritten;
// 如果aof缓冲区为空,则直接返回
if (sdslen(server.aof_buf) == 0) return;
// 写到操作系统内存缓冲区
nwritten = write(server.aof_fd, server.aof_buf, sdslen(server.aof_buf));
// aof文件大小,用于计算aof重写
server.aof_current_size += nwritten;
// aof_no_fsync_on_rewrite默认值为0,如果为1
// 有子进程在后台执行rdb或者aof重写,则不进行fsync
if (server.aof_no_fsync_on_rewrite && (server.aof_child_pid != -1 || server.rdb_child_pid != -1))
return;
// aof 同步策略 always,每个命令都进行文件同步
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
aof_fsync(server.aof_fd);
server.aof_last_fsync = server.unixtime;
// aof 同步策略 everysec,每秒进行一次文件同步
} else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC && server.unixtime > server.aof_last_fsync)) {
aof_background_fsync(server.aof_fd);
server.aof_last_fsync = server.unixtime;
}
}
三、AOF重写
触发aof重写时机
1.bgrewriteaof 命令触发
// bgrewriteaof 命令,如果
// 1. 正在执行rdb则什么也不做
// 2. 正在执行aof重写,则aof_rewrite_scheduled置为1,等待serverCron()函数检测执行,下面有代码片段。
// 3. 没有在执行rdb或者aof重写,则执行aof重写
void bgrewriteaofCommand(client *c) {
if (server.aof_child_pid != -1) {
addReplyError(c,"Background append only file rewriting already in progress");
} else if (server.rdb_child_pid != -1) {
server.aof_rewrite_scheduled = 1;
addReplyStatus(c,"Background append only file rewriting scheduled");
} else {
rewriteAppendOnlyFileBackground()
}
}
// 定时任务ServerCron()函数会执行下面代码,检测是否有bgrewriteaof命令需要延时处理的aof重写。
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 && server.aof_rewrite_scheduled) {
rewriteAppendOnlyFileBackground();
}
2.ServerCron()函数定期检测是否需要进行aof重写
/**
* aof_rewrite_perc 100
* aof_rewrite_min_size 64*1024*1024(64M)
* serverCron()函数定时检测是否需要进行aof重写
* 检测条件 :
* 1. 没有正在执行的rdb
* 2. 没有正在执行的aof重写
* 3. aof文件必须大于64M,否则不进行重写
* 4. 当前aof文件大小比上次aof重写之后大小大100%就进行重写
*/
if (server.rdb_child_pid == -1
&& server.aof_child_pid == -1
&& server.aof_current_size > server.aof_rewrite_min_size) {
long long base = server.aof_rewrite_base_size ? server.aof_rewrite_base_size : 1;
/* 超过原先aof文件的100%就重写 */
long long growth = (server.aof_current_size*100/base) - 100;
if (growth >= server.aof_rewrite_perc) {
rewriteAppendOnlyFileBackground();
}
}
对于aof重写,最终都会执行rewriteAppendOnlyFileBackground()函数,生成aof临时文件。
ServerCron()函数会检测aof重写完成后,执行aof回调函数backgroundRewriteDoneHandler()。
/* 定时任务会执行此函数,此函数做了很多事情,这里为了代码整洁,删除了其它代码 */
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
/* 如果后台正在进行rdb或者aof_rewrite,则进行检测 */
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
// wait3()函数会返回子进程的pid
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
// rdb 回调处理函数
if (pid == server.rdb_child_pid) {
backgroundSaveDoneHandler(exitcode,bysignal);
// aof_write 回调处理函数
} else if (pid == server.aof_child_pid) {
backgroundRewriteDoneHandler(exitcode,bysignal);
}
}
}
}
/* aof结束之后主线程处理(阻塞) */
void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
snprintf(tmpfile,256, "temp-rewriteaof-bg-%d.aof", (int)server.aof_child_pid);
int newfd = open(tmpfile,O_WRONLY|O_APPEND);
// aof重写缓冲区写入aof文件
aofRewriteBufferWrite(newfd)
// 替换aof文件
rename(tmpfile, server.aof_filename)
}
四、混合持久化
redis 4.0版本之后支持混合持久化。通过redis.conf配置文件配置aof-use-rdb-preamble为yes来开启混合持久化。
// aof重写
int rewriteAppendOnlyFile(char *filename) {
rio aof;
char tmpfile[256];
snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
FILE *fp = fopen(tmpfile,"w");
// 开启混合持久化 aof文件前面保存的是rdb的二进制数据,
// 定时函数执行backgroundRewriteDoneHandler回调函数,把aof重写缓冲区里面的数据写到后面
if (server.aof_use_rdb_preamble) {
rdbSaveRio(&aof,&error,RDB_SAVE_AOF_PREAMBLE,NULL);
} else {
// 如果没有开启混合持久化,则还是保存命令的resp协议内容
rewriteAppendOnlyFileRio(&aof);
}
return C_OK;
}