寫在文章開頭
為避免服務器宕機著情況導致redis
內存數據庫數據丟失,redis
默認出通過rdb
保證可靠性,本文將從源碼的角度帶讀者了解rdb
讀寫時機和寫入流程。
Hi,我是 sharkChili ,是個不斷在硬核技術上作死的 java coder ,是 CSDN的博客專家 ,也是開源項目 Java Guide 的維護者之一,熟悉 Java 也會一點 Go ,偶爾也會在 C源碼 邊緣徘徊。寫過很多有意思的技術博客,也還在研究并輸出技術的路上,希望我的文章對你有幫助,非常歡迎你關注我的公眾號: 寫代碼的SharkChili 。
因為近期收到很多讀者的私信,所以也專門創建了一個交流群,感興趣的讀者可以通過上方的公眾號獲取筆者的聯系方式完成好友添加,點擊備注 “加群” 即可和筆者和筆者的朋友們進行深入交流。
詳解RDB持久化
save指令觸發rdb
redis
支持通過命令的方式持久化內存數據庫數據,當我們鍵入save
的時候,redis
解析到這個指令之后,主線程直接調用saveCommand
方法生成rdb文件落到磁盤中。
我們可以在rdb.c
文件中看到該方法的實現,可以看到為了避免臟寫等問題,saveCommand
會檢查當前是否有rdb
子進程執行,如果沒有在子進程執行rdb持久化則直接調用rdbSave
方法生成dump.rdb
文件落盤:
//調用save指令其內部調用rdbSave完成rdb文件生成
void saveCommand(redisClient *c) {//檢查是否子進程執行rdb,若有則直接返回if (server.rdb_child_pid != -1) {addReplyError(c,"Background save already in progress");return;}//調用rdbSaveif (rdbSave(server.rdb_filename) == REDIS_OK) {addReply(c,shared.ok);} else {addReply(c,shared.err);}
}
步入rdbSave
即可看到生成臨時rdb
寫入數據,然后數據刷盤,最后完成文件名原子修改的操作:
int rdbSave(char *filename) {char tmpfile[256];FILE *fp;rio rdb;int error;//生成一個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;}//調用rdbSaveRio完成數據寫入rioInitWithFile(&rdb,fp);if (rdbSaveRio(&rdb,&error) == REDIS_ERR) {errno = error;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;//完成寫入后文件重命名為dump.rdbif (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指令觸發rdb
同時redis
也支持后臺持久化,如果用戶需要考慮redis
性能問題,可以直接通過bgsave
指令創建rdb
子進程完成數據庫數據持久化。
我們同樣可以在rdb.c
文件中看到bgsave指令調用的方法bgsaveCommand
,可以看到如果沒有子進程進行rdb
或者aof
,該指令會調用rdbSaveBackground
完成異步數據持久化:
//調用rdbSaveBackground創建一個子進程生成rdb文件,不影響主線程
void bgsaveCommand(redisClient *c) {//如果有子進程執行rdb或者aof,則直接返回錯誤提醒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) {//調用rdbSaveBackground進行數據持久化addReplyStatus(c,"Background saving started");} else {addReply(c,shared.err);}
}
步入rdbSaveBackground
可以看到,其內部還會檢查一次是否有文件進行rdb
,如果明確沒有之后直接fork一個子進程出來調用上文所說的rdbSave
完成數據持久化到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) {//創建子進程int retval;/* Child */closeListeningSockets(0);redisSetProcTitle("redis-rdb-bgsave");retval = rdbSave(filename);//生成rdb文件if (retval == REDIS_OK) {//......}exitFromChild((retval == REDIS_OK) ? 0 : 1);//退出子進程} else {//......}return REDIS_OK; /* unreached */
}
RDB被動觸發
redis
被動觸發由時間事件輪詢處理,我們可以在redis.conf
配置rdb被動觸發持久化的時機,默認配置如下當60s
生成10000
或者300
生成10
次改變亦或者900s
生成1s,我們就會執行一次被動rdb
持久化:
save 900 1
save 300 10
save 60 10000
對應的我們可以在redis.c
的serverCron
函數在看到這段邏輯,它會遍歷出我們配置的保存間隔配置saveparam
,通過比對這3條配置的上次保存時間計算出時間間隔,以及當前redis
變化書dirty看看是否符合要求,若如何要求則進行后臺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個配置的params,如果改變數和事件間隔配置要求則直接進行后臺被動rdb持久化for (j = 0; j < server.saveparamslen; j++) {struct saveparam *sp = server.saveparams+j;if (server.dirty >= sp->changes && //查看變化數是否大于當前配置的changesserver.unixtime-server.lastsave > sp->seconds && //查看時間間隔是否大于配置(server.unixtime-server.lastbgsave_try >REDIS_BGSAVE_RETRY_DELAY ||server.lastbgsave_status == REDIS_OK)){//......//執行異步持久化rdbSaveBackground(server.rdb_filename);break;}}//......}}//......return 1000/server.hz;
}
其他被動落盤時機
其實有些時候我們執行的某些執行也會進行rdb
持久化,例如flushall
刷盤指令,其調用函數flushallCommand
就會時間串行執行rdb
持久化:
//調用flush指令時會調用rdbSave進行數據持久化
void flushallCommand(redisClient *c) {//......if (server.saveparamslen > 0) {//串行執行rdb持久化int saved_dirty = server.dirty;rdbSave(server.rdb_filename);//......}server.dirty++;
}
當我們關閉redis
服務器的時候也會執行rdb
串行持久化:
//服務器進程關閉時調用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寫入文件數據詳解
無論是rdbsave
還是rdbbgsave
對應的方法,其內部都會調用rdbSaveRio
,它進行文件寫入時對應寫入數據大體順序是:
- 寫入
REDIS
大寫。 - 補0填充長度。
- 寫入當前redis版本號,以筆者源碼為例則是6。
- 遍歷數據庫寫入
REDIS_RDB_OPCODE_SELECTDB
表示開始存儲數據庫數據,這個值默認為254,redis
會轉為八進制376
寫入。 - 遍歷當前數據庫鍵值對
key
長度和key
,value
長度和value
寫入,后續數據庫都是如此往復。 - 所有數據庫寫完后補
REDIS_RDB_OPCODE_EOF
和checksum用于后續rdb數據恢復的校驗。
為保證讀者更直觀的了解redis持久化寫入的內容,我們可以刪除本地rdb文件,然后執行如下執行生成一個全新的rdb文件:
# 保存鍵值對
set key value
# 切換到1庫
select 1
# 保存鍵值對到1庫
set key-1 value
# 調用save進行數據持久化
save
正常情況下我們打開rdb
文件會得到一堆類型亂碼的內容,我們無法知曉寫入的信息,我們可以直接鍵入od
生成rdb文件16
進制數據及其對應的ASCII
字符:
od -A x -t x1c -v dump.rdb
最終我們就可以得到如下文件,可以看到數據格式和筆者上文所說基本一致:
# 大寫REDIS 補0 254的8進制 當前數據庫索引 鍵值對`key`長度和`key`,`value`長度和`value`
#000000 52 45 44 49 53 30 30 30 36 fe 00 00 03 6b 65 79R 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 05005 v a l u e
# 254的8進制 當前數據庫索引1 鍵值對key長度和key,value長度和value
376 001 \0 005 k e y - 1 005
000020 76 61 6c 75 65 ff 76 eb e4 80 bd df 66 11v a l u e
# EOF 255八進制 剩下8位是對應的checksum
377 v 353 344 200 275 337 f 021
00002e
對應的我們給出這段源碼,對應的寫入流程如上文筆者所述:
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);//對應redis 3個0 然后版本號,當前版本為6if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;//上述魔數寫入rdb文件//遍歷數據庫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;//寫入254,也就是內容中的376if (rdbSaveLen(rdb,j) == -1) goto werr;//寫入當前庫索引//遍歷當前鍵值對寫入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;//寫入鍵值對}dictReleaseIterator(di);}//....../* EOF opcode */if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;//寫入結束符254 八進制為377cksum = rdb->cksum;memrev64ifbe(&cksum);if (rioWrite(rdb,&cksum,8) == 0) goto werr;//寫入8位數校驗和,其底層調用rioGenericUpdateChecksum,按照cksum到數組中獲取就對應的值并return REDIS_OK;//......
}
對應的我們步入rdbSaveKeyValuePair
即可看到redis
獲取key
長度和key,以及value
長度和value
并寫入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;//寫入類型以字符串形式就是0if (rdbSaveStringObject(rdb,key) == -1) return -1;//寫入key長度和keyif (rdbSaveObject(rdb,val) == -1) return -1;//寫入value長度和valuereturn 1;
}
小結
自此我們將redis
持久化策略rdb都分析完成了,希望對你有幫助。
我是 sharkchili ,CSDN Java 領域博客專家,開源項目—JavaGuide contributor,我想寫一些有意思的東西,希望對你有幫助,如果你想實時收到我寫的硬核的文章也歡迎你關注我的公眾號: 寫代碼的SharkChili 。
因為近期收到很多讀者的私信,所以也專門創建了一個交流群,感興趣的讀者可以通過上方的公眾號獲取筆者的聯系方式完成好友添加,點擊備注 “加群” 即可和筆者和筆者的朋友們進行深入交流。