Redis RDB 持久化源碼深度解析:從原理到實(shí)現(xiàn)
為避免服務(wù)器宕機(jī)著情況導(dǎo)致redis內(nèi)存數(shù)據(jù)庫(kù)數(shù)據(jù)丟失,redis默認(rèn)出通過(guò)rdb保證可靠性,本文將從源碼的角度帶讀者了解rdb讀寫(xiě)時(shí)機(jī)和寫(xiě)入流程。
save指令觸發(fā)rdb
redis支持通過(guò)命令的方式持久化內(nèi)存數(shù)據(jù)庫(kù)數(shù)據(jù),當(dāng)我們鍵入save的時(shí)候,redis解析到這個(gè)指令之后,主線(xiàn)程直接調(diào)用saveCommand方法生成rdb文件落到磁盤(pán)中。
我們可以在rdb.c文件中看到該方法的實(shí)現(xiàn),可以看到為了避免臟寫(xiě)等問(wèn)題,saveCommand會(huì)檢查當(dāng)前是否有rdb子進(jìn)程執(zhí)行,如果沒(méi)有在子進(jìn)程執(zhí)行rdb持久化則直接調(diào)用rdbSave方法生成dump.rdb文件落盤(pán):
//調(diào)用save指令其內(nèi)部調(diào)用rdbSave完成rdb文件生成
void saveCommand(redisClient *c) {
//檢查是否子進(jìn)程執(zhí)行rdb,若有則直接返回
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
return;
}
//調(diào)用rdbSave
if (rdbSave(server.rdb_filename) == REDIS_OK) {
addReply(c,shared.ok);
} else {
addReply(c,shared.err);
}
}
步入rdbSave即可看到生成臨時(shí)rdb寫(xiě)入數(shù)據(jù),然后數(shù)據(jù)刷盤(pán),最后完成文件名原子修改的操作:
int rdbSave(char *filename) {
char tmpfile[256];
FILE *fp;
rio rdb;
int error;
//生成一個(gè)tmp文件
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;
}
//調(diào)用rdbSaveRio完成數(shù)據(jù)寫(xiě)入
rioInitWithFile(&rdb,fp);
if (rdbSaveRio(&rdb,&error) == REDIS_ERR) {
errno = error;
goto werr;
}
//直接刷盤(pán)到磁盤(pán),避免留在系統(tǒng)輸出緩沖區(qū)
/* 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;
//完成寫(xiě)入后文件重命名為dump.rdb
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;
}
//......
return REDIS_OK;
//......
}
bgsave指令觸發(fā)rdb
同時(shí)redis也支持后臺(tái)持久化,如果用戶(hù)需要考慮redis性能問(wèn)題,可以直接通過(guò)bgsave指令創(chuàng)建rdb子進(jìn)程完成數(shù)據(jù)庫(kù)數(shù)據(jù)持久化。
我們同樣可以在rdb.c文件中看到bgsave指令調(diào)用的方法bgsaveCommand,可以看到如果沒(méi)有子進(jìn)程進(jìn)行rdb或者aof,該指令會(huì)調(diào)用rdbSaveBackground完成異步數(shù)據(jù)持久化:
//調(diào)用rdbSaveBackground創(chuàng)建一個(gè)子進(jìn)程生成rdb文件,不影響主線(xiàn)程
void bgsaveCommand(redisClient *c) {
//如果有子進(jìn)程執(zhí)行rdb或者aof,則直接返回錯(cuò)誤提醒
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
} else if (server.aof_child_pid != -1) {
addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");
} else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {//調(diào)用rdbSaveBackground進(jìn)行數(shù)據(jù)持久化
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}
步入rdbSaveBackground可以看到,其內(nèi)部還會(huì)檢查一次是否有文件進(jìn)行rdb,如果明確沒(méi)有之后直接fork一個(gè)子進(jìn)程出來(lái)調(diào)用上文所說(shuō)的rdbSave完成數(shù)據(jù)持久化到dump.rdb中:
int rdbSaveBackground(char *filename) {
pid_t childpid;
long long start;
if (server.rdb_child_pid != -1) return REDIS_ERR;
//......
start = ustime();
if ((childpid = fork()) == 0) {//創(chuàng)建子進(jìn)程
int retval;
//......
retval = rdbSave(filename);//生成rdb文件
exitFromChild((retval == REDIS_OK) ? 0 : 1);//退出子進(jìn)程
} else {
//......
}
return REDIS_OK; /* unreached */
}
rdb被動(dòng)觸發(fā)
redis被動(dòng)觸發(fā)由時(shí)間事件輪詢(xún)處理,我們可以在redis.conf配置rdb被動(dòng)觸發(fā)持久化的時(shí)機(jī),默認(rèn)配置如下當(dāng)60s生成10000或者300s 生成10次改變亦或者900s生成1次改變,我們就會(huì)執(zhí)行一次被動(dòng)rdb持久化:
save 900 1
save 300 10
save 60 10000
對(duì)應(yīng)的我們可以在redis.c的serverCron函數(shù)在看到這段邏輯,它會(huì)遍歷出我們配置的保存間隔配置saveparam,通過(guò)比對(duì)這3條配置的上次保存時(shí)間計(jì)算出時(shí)間間隔,以及當(dāng)前redis變化書(shū)dirty看看是否符合要求,若如何要求則進(jìn)行后臺(tái)rdb持久化:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
//......
/* Check if a background saving or AOF rewrite in progress terminated. */
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
//......
}
} else {
//遍歷3個(gè)配置的params,如果改變數(shù)和事件間隔配置要求則直接進(jìn)行后臺(tái)被動(dòng)rdb持久化
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
if (server.dirty >= sp->changes && //查看變化數(shù)是否大于當(dāng)前配置的changes
server.unixtime-server.lastsave > sp->seconds && //查看時(shí)間間隔是否大于配置
(server.unixtime-server.lastbgsave_try >
REDIS_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == REDIS_OK))
{
//......
//執(zhí)行異步持久化
rdbSaveBackground(server.rdb_filename);
break;
}
}
//......
}
}
//......
return 1000/server.hz;
}
其他被動(dòng)落盤(pán)時(shí)機(jī)
其實(shí)有些時(shí)候我們執(zhí)行的某些執(zhí)行也會(huì)進(jìn)行rdb持久化,例如flushall刷盤(pán)指令,其調(diào)用函數(shù)flushallCommand就會(huì)時(shí)間串行執(zhí)行rdb持久化:
//調(diào)用flush指令時(shí)會(huì)調(diào)用rdbSave進(jìn)行數(shù)據(jù)持久化
void flushallCommand(redisClient *c) {
//......
if (server.saveparamslen > 0) {
//串行執(zhí)行rdb持久化
int saved_dirty = server.dirty;
rdbSave(server.rdb_filename);
//......
}
server.dirty++;
}
當(dāng)我們關(guān)閉redis服務(wù)器的時(shí)候也會(huì)執(zhí)行rdb串行持久化:
//服務(wù)器進(jìn)程關(guān)閉時(shí)調(diào)用rdbSave生成rdb文件
int prepareForShutdown(int flags) {
//......
if (server.rdb_child_pid != -1) {
//......
}
if (server.aof_state != REDIS_AOF_OFF) {
//......
}
if ((server.saveparamslen > 0 && !nosave) || save) {
if (rdbSave(server.rdb_filename) != REDIS_OK) {
//......
return REDIS_ERR;
}
}
//......
return REDIS_OK;
}
rdb寫(xiě)入文件數(shù)據(jù)詳解
無(wú)論是rdbsave還是rdbbgsave對(duì)應(yīng)的方法,其內(nèi)部都會(huì)調(diào)用rdbSaveRio,它進(jìn)行文件寫(xiě)入時(shí)對(duì)應(yīng)寫(xiě)入數(shù)據(jù)大體順序是:
- 寫(xiě)入REDIS大寫(xiě)。
- 補(bǔ)0填充長(zhǎng)度。
- 寫(xiě)入當(dāng)前redis版本號(hào),以筆者源碼為例則是6。
- 遍歷數(shù)據(jù)庫(kù)寫(xiě)入REDIS_RDB_OPCODE_SELECTDB表示開(kāi)始存儲(chǔ)數(shù)據(jù)庫(kù)數(shù)據(jù),這個(gè)值默認(rèn)為254,redis會(huì)轉(zhuǎn)為八進(jìn)制376寫(xiě)入。
- 遍歷當(dāng)前數(shù)據(jù)庫(kù)鍵值對(duì)key長(zhǎng)度和key,value長(zhǎng)度和value寫(xiě)入,后續(xù)數(shù)據(jù)庫(kù)都是如此往復(fù)。
- 所有數(shù)據(jù)庫(kù)寫(xiě)完后補(bǔ)上REDIS_RDB_OPCODE_EOF和checksum用于后續(xù)rdb數(shù)據(jù)恢復(fù)的校驗(yàn)。
為保證讀者更直觀的了解redis持久化寫(xiě)入的內(nèi)容,我們可以刪除本地rdb文件,然后執(zhí)行如下執(zhí)行生成一個(gè)全新的rdb文件:
# 保存鍵值對(duì)
set key value
# 切換到1庫(kù)
select 1
# 保存鍵值對(duì)到1庫(kù)
set key-1 value
# 調(diào)用save進(jìn)行數(shù)據(jù)持久化
save
正常情況下我們打開(kāi)rdb文件會(huì)得到一堆類(lèi)型亂碼的內(nèi)容,我們無(wú)法知曉寫(xiě)入的信息,我們可以直接鍵入od生成rdb文件16進(jìn)制數(shù)據(jù)及其對(duì)應(yīng)的ASCII字符:
od -A x -t x1c -v dump.rdb
最終我們就可以得到如下文件,可以看到數(shù)據(jù)格式和筆者上文所說(shuō)基本一致:
# 大寫(xiě)REDIS 補(bǔ)0 254的8進(jìn)制 當(dāng)前數(shù)據(jù)庫(kù)索引 鍵值對(duì)`key`長(zhǎng)度和`key`,`value`長(zhǎng)度和`value`
#000000 52 45 44 49 53 30 30 30 36 fe 00 00 03 6b 65 79
R E D I S 0 0 0 6 376 \0 \0 003 k e y
000010 05 76 61 6c 75 65 fe 01 00 05 6b 65 79 2d 31 05
005 v a l u e
# 254的8進(jìn)制 當(dāng)前數(shù)據(jù)庫(kù)索引1 鍵值對(duì)key長(zhǎng)度和key,value長(zhǎng)度和value
376 001 \0 005 k e y - 1 005
000020 76 61 6c 75 65 ff 76 eb e4 80 bd df 66 11
v a l u e
# EOF 255八進(jìn)制 剩下8位是對(duì)應(yīng)的checksum
377 v 353 344 200 275 337 f 021
00002e
對(duì)應(yīng)的我們給出這段源碼,對(duì)應(yīng)的寫(xiě)入流程如上文筆者所述:
int rdbSaveRio(rio *rdb, int *error) {
dictIterator *di = NULL;
dictEntry *de;
char magic[10];
int j;
long long now = mstime();
uint64_t cksum;
if (server.rdb_checksum)
rdb->update_cksum = rioGenericUpdateChecksum;
snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);//對(duì)應(yīng)redis 3個(gè)0 然后版本號(hào),當(dāng)前版本為6
if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;//上述魔數(shù)寫(xiě)入rdb文件
//遍歷數(shù)據(jù)庫(kù)
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) return REDIS_ERR;
/* Write the SELECT DB opcode */
if (rdbSaveType(rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;//寫(xiě)入254,也就是內(nèi)容中的376
if (rdbSaveLen(rdb,j) == -1) goto werr;//寫(xiě)入當(dāng)前庫(kù)索引
//遍歷當(dāng)前鍵值對(duì)寫(xiě)入
while((de = dictNext(di)) != NULL) {
sds keystr = dictGetKey(de);
robj key, *o = dictGetVal(de);
long long expire;
initStaticStringObject(key,keystr);
expire = getExpire(db,&key);
if (rdbSaveKeyValuePair(rdb,&key,o,expire,now) == -1) goto werr;//寫(xiě)入鍵值對(duì)
}
dictReleaseIterator(di);
}
//......
/* EOF opcode */
if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;//寫(xiě)入結(jié)束符254 八進(jìn)制為377
cksum = rdb->cksum;
memrev64ifbe(&cksum);
if (rioWrite(rdb,&cksum,8) == 0) goto werr;//寫(xiě)入8位數(shù)校驗(yàn)和,其底層調(diào)用rioGenericUpdateChecksum,按照cksum到數(shù)組中獲取就對(duì)應(yīng)的值并
return REDIS_OK;
//......
}
對(duì)應(yīng)的我們步入rdbSaveKeyValuePair即可看到redis獲取key長(zhǎng)度和key,以及value長(zhǎng)度和value并寫(xiě)入rdb文件的核心流程:
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
long long expiretime, long long now)
{
//......
/* Save type, key, value */
if (rdbSaveObjectType(rdb,val) == -1) return -1;//寫(xiě)入類(lèi)型以字符串形式就是0
if (rdbSaveStringObject(rdb,key) == -1) return -1;//寫(xiě)入key長(zhǎng)度和key
if (rdbSaveObject(rdb,val) == -1) return -1;//寫(xiě)入value長(zhǎng)度和value
return 1;
}