一、前言
Redis是key-value型的内存数据库,所有的数据都保存在内存中,如果一段断电或者服务器宕机那么数据将会全部丢失,这很显然满足不了很多业务场景的需求,比如我的Redis数据库中存放很多待执行的URL爬取任务,如果在运行过程中服务器突然断电,所有的数据及没有处理的任务都会丢失,因此为了解决这个问题Redis引入了RDB持久化功能,此外Redis还提供另外一种更高级的持久化功能AOF持久化,先来看下RDB持久化的实现机制等。
二、RDB持久化
2.1 RDB文件格式
在介绍持久化过程之前,我们来先看下RDB文件的格式
再看下key_value_pairs部分的结构,RDB文件中的key_value_pairs部分都保存了一个或以上数量的键值对,包括键值对的过期信息等,
2.2 RDB文件的创建与载入
Redis中有两个命令可以生成RDB文件,一个是SAVE,另外一个是BGSAVE
SAVE命令:SAVE命令会阻塞Redis服务器进程,指导RDB文件创建完毕,在这个期间服务器进程不能处理任何来自客户端的命令请求。
BGSAVE命令:BGSAVE命令会从服务器进程中派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程继续处理命令请求
创建RDB文件过程由rdb.c/rdbSave函数完成
/* 后台进行rbd保存操作 */
int rdbSaveBackground(char *filename) {
pid_t childpid;
long long start;
if (server.rdb_child_pid != -1) return REDIS_ERR;
// 距离上一次成功执行SAVE或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
// 获取当前系统时间
start = ustime();
//利用fork()创建子进程用来实现rdb的保存操作
//此时有2个进程在执行这段函数的代码,在子进行程返回的pid为0,
//所以会执行下面的代码,在父进程中返回的代码为孩子的pid,不为0,所以执行else分支的代码
//在父进程中放返回-1代表创建子进程失败
if ((childpid = fork()) == 0) {
//在这个if判断的代码就是在子线程中后执行的操作
int retval;
/* Child */
// 由于子进程会和父进程共享打开的Socket文件描述句柄,所以要在子进程中关闭其
closeListeningSockets(0);
redisSetProcTitle("redis-rdb-bgsave");
//这个就是刚刚说的rdbSave()操作
// 开始执行save过程
retval = rdbSave(filename);
if (retval == REDIS_OK) {
size_t private_dirty = zmalloc_get_private_dirty();
if (private_dirty) {
redisLog(REDIS_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
}
exitFromChild((retval == REDIS_OK) ? 0 : 1);
} else {
//执行父线程的后续操作
/* Parent */
server.stat_fork_time = ustime()-start;
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
if (childpid == -1) {
server.lastbgsave_status = REDIS_ERR;
redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return REDIS_ERR;
}
redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
server.rdb_save_time_start = time(NULL);
server.rdb_child_pid = childpid;
updateDictResizePolicy();
return REDIS_OK;
}
return REDIS_OK; /* unreached */
}
/* 保存rdb数据库的内容到磁盘中 */
int rdbSave(char *filename) {
dictIterator *di = NULL;
dictEntry *de;
char tmpfile[256];
char magic[10];
int j;
long long now = mstime();
FILE *fp;
rio rdb;
uint64_t cksum;
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
fp = fopen(tmpfile,"w");
if (!fp) {
//随时记录错误日志,此时为文件无法打开的时候
redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
strerror(errno));
return REDIS_ERR;
}
//初始化rbd和fp的初始操作,据此判断,后面关于rdb的操作都存入到fp这个文件中
// 使用rio进行健壮的I/O读写,将文件句柄与读缓冲区联系起来
rioInitWithFile(&rdb,fp);
if (server.rdb_checksum)
rdb.update_cksum = rioGenericUpdateChecksum;
snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
//循环服务端下的所有数据库
for (j = 0; j < server.dbnum; j++) {
//获取单个数据库
redisDb *db = server.db+j;
dict *d = db->dict;
if (dictSize(d) == 0) continue;
di = dictGetSafeIterator(d);
if (!di) {
fclose(fp);
return REDIS_ERR;
}
/* Write the SELECT DB opcode */
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;
if (rdbSaveLen(&rdb,j) == -1) goto werr;
/* Iterate this DB writing every entry */
while((de = dictNext(di)) != NULL) {
sds keystr = dictGetKey(de);
robj key, *o = dictGetVal(de);
long long expire;
initStaticStringObject(key,keystr);
expire = getExpire(db,&key);
//将里面的键值存入rdb中
if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;
}
dictReleaseIterator(di);
}
di = NULL; /* So that we don't release it again on error. */
/* EOF opcode */
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
/* CRC64 checksum. It will be zero if checksum computation is disabled, the
* loading code skips the check in this case. */
cksum = rdb.cksum;
memrev64ifbe(&cksum);
if (rioWrite(&rdb,&cksum,8) == 0) goto werr;
/* Make sure data will not remain on the OS's output buffers */
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok. */
if (rename(tmpfile,filename) == -1) {
redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
unlink(tmpfile);
return REDIS_ERR;
}
redisLog(REDIS_NOTICE,"DB saved on disk");
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = REDIS_OK;
return REDIS_OK;
werr:
//这里又用到了goto处理异常操作的代码
fclose(fp);
unlink(tmpfile);
redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
if (di) dictReleaseIterator(di);
return REDIS_ERR;
}
在具体的文件写入过程中,作者使用了自己包装的rio包,实现带缓冲、健壮的读写文件,
首先使用rio进行健壮的I/O读写,将文件句柄与读缓冲区联系起来
// 使用rio进行健壮的I/O读写,将文件句柄与读缓冲区联系起来
rioInitWithFile(&rdb,fp);
/* 根据上面描述的方法,定义了FileRio */
static const rio rioFileIO = {
rioFileRead,
rioFileWrite,
rioFileTell,
NULL, /* update_checksum */
0, /* current checksum */
0, /* bytes read or written */
0, /* read/write chunk size */
{ { NULL, 0 } } /* union for io-specific vars */
};
/* 初始化rio中的file变量 */
void rioInitWithFile(rio *r, FILE *fp) {
*r = rioFileIO;
r->io.file.fp = fp;
r->io.file.buffered = 0;
r->io.file.autosync = 0;
}
最终的实现由rio.h中的rioWrite函数实现带缓冲的读写
static inline size_t rioWrite(rio *r, const void *buf, size_t len) {
while (len) {
//判断当前操作字节长度是否超过最大长度
size_t bytes_to_write = (r->max_processing_chunk && r->max_processing_chunk < len) ? r->max_processing_chunk : len;
//写入新的数据时,更新校验和
if (r->update_cksum) r->update_cksum(r,buf,bytes_to_write);
//执行写方法
if (r->write(r,buf,bytes_to_write) == 0)
return 0;
buf = (char*)buf + bytes_to_write;
len -= bytes_to_write;
//操作字节数增加
r->processed_bytes += bytes_to_write;
}
return 1;
}