redis是内存数据库,如果数据没有写入磁盘,一旦服务器挂掉或者进程退出,那么数据也就不见了。
redis通过将数据保存到一个RDB文件中来实现持久化。
RDB文件是一个经过压缩的二进制文件,保存在硬盘上,这样即便是服务器挂掉或者进程退出,数据也不会丢失。在服务器启动时会载入RDB文件。
redis提供了两种命令来创建RDB文件:SAVE命令和BGSAVE命令。
客户端输入SAVE命令
void saveCommand(client *c) {
/* 判断是否已经有bgsave操作 */
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
return;
}
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
addReply(c,shared.ok);
} else {
addReply(c,shared.err);
}
}
这里关注rdbSave方法,就是SAVE命令的具体实现:
int rdbSave(char *filename, rdbSaveInfo *rsi) {
char tmpfile[256];
char cwd[MAXPATHLEN]; /* 存放错误信息 */
FILE *fp;
rio rdb;
int error = 0;
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
fp = fopen(tmpfile,"w"); /* 调用函数库的fopen函数,创建(打开)只写文件 */
if (!fp) {/* 创建(打开)失败 */
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Failed opening the RDB file %s (in server root dir %s) "
"for saving: %s",
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
return C_ERR;
}
/* 初始化文件 */
rioInitWithFile(&rdb,fp);
/* 以RDB格式生成数据库转储文件,并将其发送到指定的Redis I/O通道 */
if (rdbSaveRio(&rdb,&error,RDB_SAVE_NONE,rsi) == C_ERR) {
errno = error;
goto werr;
}
/* 确保数据不会留在操作系统的输出缓冲区中 */
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
/* 调用函数库的rename方法 */
if (rename(tmpfile,filename) == -1) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Error moving temp DB file %s on the final "
"destination %s (in server root dir %s): %s",
tmpfile,
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
unlink(tmpfile);
return C_ERR;
}
serverLog(LL_NOTICE,"DB saved on disk");
/* 设置一些参数 */
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
return C_OK;
werr:
serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
fclose(fp);
unlink(tmpfile);
return C_ERR;
}
其中涉及到rio对象的一些操作。redis将I/O层封装起来成rio对象,文件读写和buffer都基于它来操作。这里不展开来说。
客户端输入BGSAVE(background save)命令
void bgsaveCommand(client *c) {
int schedule = 0;
/* 校验schedule参数 */
if (c->argc > 1) {
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
schedule = 1;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
} else if (server.aof_child_pid != -1) {/* 如果这时有AOF操作 */
if (schedule) {/* 给schedule参数重新赋值 */
server.rdb_bgsave_scheduled = 1;
addReplyStatus(c,"Background saving scheduled");
} else {/* 否则抛错:AOF和BGSAVE不能同时进行 */
addReplyError(c,
"An AOF log rewriting in progress: can't BGSAVE right now. "
"Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever "
"possible.");
}
} else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {/* BGSAVE */
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}
这里关注rdbSaveBackground方法:
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
long long start;
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
/* 打开子-父通道 */
openChildInfoPipe();
start = ustime();
/* fork出子进程 */
if ((childpid = fork()) == 0) {
int retval;
/* 以下是子进程的操作 */
closeListeningSockets(0);
redisSetProcTitle("redis-rdb-bgsave");
/* 由子进程去完成save操作 */
retval = rdbSave(filename,rsi);
if (retval == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
serverLog(LL_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
server.child_info_data.cow_size = private_dirty;
/* 向父进程发送信号 */
sendChildInfo(CHILD_INFO_TYPE_RDB);
}
/* 退出子进程 */
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* 以下是父进程的操作 */
server.stat_fork_time = ustime()-start;
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024);
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
if (childpid == -1) {
closeChildInfoPipe();
server.lastbgsave_status = C_ERR;
serverLog(LL_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return C_ERR;
}
serverLog(LL_NOTICE,"Background saving started by pid %d",childpid);
server.rdb_save_time_start = time(NULL);
server.rdb_child_pid = childpid;
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
updateDictResizePolicy();
return C_OK;
}
return C_OK;
}
通过SAVE和BGSAVE的实现可以看出,SAVE命令会阻塞服务器直到RDB文件创建完毕,但是BGSAVE命令会fork出新的子进程来负责RDB文件的创建,而父进程也就是服务器进程可以继续处理命令请求,不会被阻塞(可以参考java中的fork-join框架)。
因此,可以通过设置save选项,让服务器每隔一段时间自动的执行一次BGSAVE。
在redis.conf中有这么一段:
这里默认设置了三个条件,他们的意思分别是:
只要这三个条件中的任意一条符合,那么服务器就会执行BGSAVE。
这些默认条件会保存在redisServer结构的saveparam数组中:
struct redisServer {
long long dirty;
time_t lastsave;
struct saveparam *saveparams;
//...
};
saveparam结构如下:
struct saveparam {
time_t seconds; /* 秒 */
int changes; /* 修改次数 */
};
redisServer中还有两个属性:
dirty:记录上一次SAVE或者BGSAVE之后,服务器对所有的数据库做了多少次修改;
lastsave:记录了上一次服务器成功执行SAVE或BGSAVE的时间。
服务器会周期性执行serverCron函数,如下,这个函数会去检查此时是否满足save选项设置的条件,如果满足那么就会执行BGSAVE:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
//...
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
//...
} else {
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
/* 校验是否满足save条件 */
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))
{
serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
sp->changes, (int)sp->seconds);
/* 满足条件,执行BGSAVE */
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
rdbSaveBackground(server.rdb_filename,rsiptr);
break;
}
}
//...
}
//...
return 1000/server.hz;
}
int rdbLoad(char *filename, rdbSaveInfo *rsi) {
FILE *fp;
rio rdb;
int retval;
if ((fp = fopen(filename,"r")) == NULL) return C_ERR;
startLoading(fp);
rioInitWithFile(&rdb,fp);
retval = rdbLoadRio(&rdb,rsi,0);
fclose(fp);
stopLoading();
return retval;
}
通过rio对象来操作load。可以看出load操作会阻塞服务器直到load完毕。