深入剖析Redis RDB持久化机制
本文來自@凡趣科技?pesiwang同學的投稿分享,對Redis?RDB文件持久化的內(nèi)部實現(xiàn)進行了源碼分析。
本文分析源碼基于?Redis 2.4.7 stable?版本。下面是其文章原文:
rdb是redis保存內(nèi)存數(shù)據(jù)到磁盤數(shù)據(jù)的其中一種方式(另一種是AOF)。Rdb的主要原理就是在某個時間點把內(nèi)存中的所有數(shù)據(jù)的快照保存一份到磁盤上。在條件達到時通過fork一個子進程把內(nèi)存中的數(shù)據(jù)寫到一個臨時文件中來實現(xiàn)保存數(shù)據(jù)快照。在所有數(shù)據(jù)寫完后再把這個臨時文件用原子函數(shù)rename(2)重命名為目標rdb文件。這種實現(xiàn)方式充分利用fork的copy on write。
另外一種是通過save命令主動觸發(fā)保存數(shù)據(jù)快照,這種是阻塞式的,即不會通過生成子進程來進行數(shù)據(jù)集快照的保存。
相關配置
save <seconds> <changes>經(jīng)過多少秒且多少個key有改變就進行,可以配置多個,只要有一個滿足就進行保存數(shù)據(jù)快照到磁盤
rdbcompression yes保存數(shù)據(jù)到rdb文件時是否進行壓縮,如果不想可以配置成’no’,默認是’yes’,因為壓縮可以減少I/O,當然,壓縮需要消耗一些cpu資源。
dbfilename dump.rdb快照文件名
dir ./快照文件所在的目錄,同時也是AOF文件所在的目錄
Rdb文件格式
[注:本節(jié)所說的類型,值在沒有特別標明的情況下都是針對rdb文件來說的]
Rdb文件的整體格式
文件簽名 | 版本號 | 類型 | 值 | 類型 | 值 | … | 類型 | 值
[注:豎線和空格是為了便于閱讀而加入的,rdb文件中是沒有豎線和空格分隔的]
- 文件簽名是字符串:REDIS
- 版本號是字符串:0002
- 類型是指值的類型,redis值的類型有很多種,下邊一一介紹
- 值是對應的類型下的值,不同類型的值格式不一樣。這里的值包含了redis中的key與val。而不是單指redis中val。
REDIS_SELECTDB類型與REDIS_EOF類型
- REDIS_SELECTDB類型:對應的值是redis db的編號,從0開始到比db數(shù)小1的數(shù)值。redis中可以配置db數(shù),每個key只屬于一個db。
- 存儲redis db的編號時使用的是存儲長度時使用的格式,為了盡量壓縮rdb文件,存儲長度使用的字節(jié)數(shù)是不一樣的,具體見下邊rdb中長度的存儲
- REDIS_EOF類型:沒有對應的值。rdb文件的結(jié)束符。
把這REDIS_SELECTDB類型和REDIS_EOF類型代入到上邊的rdb文件的格式中,那么rdb文件的整體格式變成為:
文件簽名 | 版本號 | REDIS_SELECTDB類型 | db編號 | 類型 | 值 | … | REDIS_SELECTD 類型 | db編號 | 類型 | 值 | … | REDIS_EOF類型
- 每個db編號后邊到下一個REDIS_SELECTDB類型出現(xiàn)之前的數(shù)據(jù)都是該db下邊的key和value的數(shù)據(jù)
相關代碼
Rdb.c:394
int rdbSave(char *filename) {…fp = fopen(tmpfile,"w");if (!fp) {redisLog(REDIS_WARNING, "Failed saving the DB: %s", strerror(errno));return REDIS_ERR;}if (fwrite("REDIS0002",9,1,fp) == 0) goto werr;for (j = 0; j < server.dbnum; j ) {…/* Write the SELECT DB opcode */if (rdbSaveType(fp,REDIS_SELECTDB) == -1) goto werr;if (rdbSaveLen(fp,j) == -1) goto werr;/* Iterate this DB writing every entry */while((de = dictNext(di)) != NULL) {…initStaticStringObject(key,keystr);expiretime = getExpire(db,&key);/* Save the expire time */if (expiretime != -1) {/* If this key is already expired skip it */if (expiretime < now) continue;if (rdbSaveType(fp,REDIS_EXPIRETIME) == -1) goto werr;if (rdbSaveTime(fp,expiretime) == -1) goto werr;}/* Save the key and associated value. This requires special* handling if the value is swapped out. */if (!server.vm_enabled || o->storage == REDIS_VM_MEMORY ||o->storage == REDIS_VM_SWAPPING) {int otype = getObjectSaveType(o);/* Save type, key, value */if (rdbSaveType(fp,otype) == -1) goto werr;if (rdbSaveStringObject(fp,&key) == -1) goto werr;if (rdbSaveObject(fp,o) == -1) goto werr;} else {/* REDIS_VM_SWAPPED or REDIS_VM_LOADING */robj *po;/* Get a preview of the object in memory */po = vmPreviewObject(o);/* Save type, key, value */if (rdbSaveType(fp,getObjectSaveType(po)) == -1)goto werr;if (rdbSaveStringObject(fp,&key) == -1) goto werr;if (rdbSaveObject(fp,po) == -1) goto werr;/* Remove the loaded object from memory */decrRefCount(po);}}dictReleaseIterator(di);}/* EOF opcode */if (rdbSaveType(fp,REDIS_EOF) == -1) goto werr;… }Rdb中長度的存儲
Redis為了盡量壓縮rdb文件真是費盡心思,先來看看redis為了壓縮使用的長度存儲。長度主要用在字符串長度,鏈表長度,hash表的大小存儲上。
Redis把長度的存儲分為四種,最左邊字節(jié)的從左到右的前兩位用于區(qū)分長度的存儲類型。
| 類型位表示 | 類型整型表示 | 占用字節(jié)數(shù) | 類型解析 |
| 00 | 0 | 1 | 當長度能用6位表示使用此類型 |
| 01 | 1 | 2 | 當長度不能用6位表示且能用14位表示使用此類型 |
| 10 | 2 | 5 | 當長度不能用14位表示且能用32位表示使用此類型 |
相關代碼
Rdb.c:31
int rdbSaveLen(FILE *fp, uint32_t len) {unsigned char buf[2];int nwritten;if (len < (1<<6)) {/* Save a 6 bit len */buf[0] = (len&0xFF)|(REDIS_RDB_6BITLEN<<6);if (rdbWriteRaw(fp,buf,1) == -1) return -1;nwritten = 1;} else if (len < (1<<14)) {/* Save a 14 bit len */buf[0] = ((len>>8)&0xFF)|(REDIS_RDB_14BITLEN<<6);buf[1] = len&0xFF;if (rdbWriteRaw(fp,buf,2) == -1) return -1;nwritten = 2;} else {/* Save a 32 bit len */buf[0] = (REDIS_RDB_32BITLEN<<6);if (rdbWriteRaw(fp,buf,1) == -1) return -1;len = htonl(len);if (rdbWriteRaw(fp,&len,4) == -1) return -1;nwritten = 1 4;}return nwritten; }也許你發(fā)現(xiàn)了,上邊的表格中只有3種,還有一種哪去了呢?
把這種特別放開是因為這種比較特殊
| 類型位表示 | 類型整型表示 | 字節(jié)后6位含義 | 類型解析 |
| 11 | 3 | 編碼類型 | 如果字符串是通過編碼后存儲的,則存儲長度的類型的位表示為11,然后根據(jù)后6位的編碼類型來確定怎樣讀取和解析接下來的數(shù)據(jù) |
是不是覺得這種長度類型很奇怪,為什么要這樣做?
Redis在兩種情況下需要對存儲的內(nèi)容進行編碼
1.把字符串轉(zhuǎn)成整數(shù)存儲
比如:‘-100’需要4個字節(jié)存儲,轉(zhuǎn)換整數(shù)只需要一個字節(jié)
相關函數(shù)rdbTryIntegerEncoding(rdb.c:88)
2.使用lzf算法壓縮字符串
相關函數(shù)lzf_compress(lzf_c.c:99),lzf的算法解釋見lzf字符串壓縮算法
當redis使用這兩種編碼對字符串進行編碼時,在讀取時需要區(qū)分改字符串有沒有被編碼過,對編碼過的字符串需要特別處理,因為長度信息是存儲在字符串的前面得,所以可以通過在存儲長度的位置上加入編碼類型的信息。
我們來看看相關代碼
Rdb.c:557
uint32_t rdbLoadLen(FILE *fp, int *isencoded) {unsigned char buf[2];uint32_t len;int type;if (isencoded) *isencoded = 0;if (fread(buf,1,1,fp) == 0) return REDIS_RDB_LENERR;type = (buf[0]&0xC0)>>6;if (type == REDIS_RDB_6BITLEN) {/* Read a 6 bit len */return buf[0]&0x3F;} else if (type == REDIS_RDB_ENCVAL) {/* Read a 6 bit len encoding type */if (isencoded) *isencoded = 1;return buf[0]&0x3F;} else if (type == REDIS_RDB_14BITLEN) {/* Read a 14 bit len */if (fread(buf 1,1,1,fp) == 0) return REDIS_RDB_LENERR;return ((buf[0]&0x3F)<<8)|buf[1];} else {/* Read a 32 bit len */if (fread(&len,4,1,fp) == 0) return REDIS_RDB_LENERR;return ntohl(len);} }- 我們可以看到,在讀取rdb文件時,當發(fā)現(xiàn)長度類型是REDIS_RDB_ENCVAL,把編碼類型返回。
我們來看看知道編碼類型后的處理
Rdb.c:633
robj *rdbGenericLoadStringObject(FILE*fp, int encode) {int isencoded;uint32_t len;sds val;len = rdbLoadLen(fp,&isencoded);if (isencoded) {switch(len) {case REDIS_RDB_ENC_INT8:case REDIS_RDB_ENC_INT16:case REDIS_RDB_ENC_INT32:return rdbLoadIntegerObject(fp,len,encode);case REDIS_RDB_ENC_LZF:return rdbLoadLzfStringObject(fp);default:redisPanic("Unknown RDB encoding type");}}if (len == REDIS_RDB_LENERR) return NULL;val = sdsnewlen(NULL,len);if (len && fread(val,len,1,fp) == 0) {sdsfree(val);return NULL;}return createObject(REDIS_STRING,val); }- 讀取長度
- 如果長度類型是有編碼信息的,則根據(jù)編碼類型進行讀取
- 如果長度類型是有效長度,則根據(jù)長度信息讀取字符串
REDIS_EXPIRETIME類型
- 如果一個key被expire設置過,那么在該key與value的前面會有一個REDIS_EXPIRETIME類型與其對應的值。
- REDIS_EXPIRETIME類型對應的值是過期時間點的timestamp
- REDIS_EXPIRETIME類型與其值是可選的,不是必須的,只有被expire設置過的key才有這個值
假設有一個key被expire命令設置過,把這REDIS_EXPIRETIME類型代入到上邊的rdb文件的格式中,那么rdb文件的整體格式變成為:
文件簽名 | 版本號 | REDIS_SELECTDB類型 | db編號 | REDIS_EXPIRETIME類型 | timestamp | 類型 | 值 | … | REDIS_SELECTD 類型 | db編號 | 類型 | 值 | … | REDIS_EOF類型
數(shù)據(jù)類型
數(shù)據(jù)類型主要有以下類型:
- REDIS_STRING類型
- REDIS_LIST類型
- REDIS_SET類型
- REDIS_ZSET類型
- REDIS_HASH類型
- REDIS_VMPOINTER類型
- REDIS_HASH_ZIPMAP類型
- REDIS_LIST_ZIPLIST類型
- REDIS_SET_INTSET類型
- REDIS_ZSET_ZIPLIST類型
其中REDIS_HASH_ZIPMAP,REDIS_LIST_ZIPLIST,REDIS_SET_INTSET和REDIS_ZSET_ZIPLIST這四種數(shù)據(jù)類型都是只在rdb文件中才有的類型,其他的數(shù)據(jù)類型其實就是val對象中type字段存儲的值。
下邊以REDIS_STRING類型和REDIS_LIST類型為例進行詳解,其他類型都類似
REDIS_STRING類型
假設rdb文件中有一個值是REDIS_STRING類型,比如執(zhí)行了一個set mykey myval命令,則在rdb文件表示為:
REDIS_STRING類型 | 值
其中值包含了key的長度,key的值,val的長度和val的值,把REDIS_STRING類型值的格式代入得:
REDIS_STRING類型 | keylen | mykey | vallen | myval
長度的存儲格式見rdb中長度的存儲
REDIS_LIST類型
1.List
REDIS_LIST | listlen | len | value | len | value
Listlen是鏈表長度
Len是鏈表結(jié)點的值value的長度
Value是鏈表結(jié)點的值
2.Ziplist
REDIS_ENCODING_ZIPLIST | ziplist
Ziplist就是通過字符串來實現(xiàn)的,直接將其存儲于rdb文件中即可
快照保存
我們接下來看看具體實現(xiàn)細節(jié)
不管是觸發(fā)條件滿足后通過fork子進程來保存快照還是通過save命令來觸發(fā),其實都是調(diào)用的同一個函數(shù)rdbSave(rdb.c:394)。
先來看看觸發(fā)條件滿足后通過fork子進程的實現(xiàn)保存快照的的實現(xiàn)
在每100ms調(diào)用一次的serverCron函數(shù)中會對快照保存的條件進行檢查,如果滿足了則進行快照保存
Redis.c:604
/* Check if a background saving or AOF rewrite in progress terminated */if (server.bgsavechildpid != -1 || server.bgrewritechildpid != -1) {int statloc;pid_t pid;if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {if (pid == server.bgsavechildpid) {backgroundSaveDoneHandler(statloc);} …updateDictResizePolicy();}} else {time_t now = time(NULL);/* If there is not a background saving in progress check if* we have to save now */for (j = 0; j < server.saveparamslen; j ) {struct saveparam *sp = server.saveparams j;if (server.dirty >= sp->changes &&now-server.lastsave > sp->seconds) {redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving…",sp->changes, sp->seconds);rdbSaveBackground(server.dbfilename);break;}}… }- 如果后端有寫rdb的子進程或者寫aof的子進程,則檢查rdb子進程是否退出了,如果退出了則進行一些收尾處理,比如更新臟數(shù)據(jù)計數(shù)server.dirty和最近快照保存時間server.lastsave。
- 如果后端沒有寫rdb的子進程且沒有寫aof的子進程,則判斷下是否有觸發(fā)寫rdb的條件滿足了,如果有條件滿足,則通過調(diào)用rdbSaveBackground函數(shù)進行快照保存。
跟著進rdbSaveBackground函數(shù)里邊看看
Rdb.c:499
int rdbSaveBackground(char *filename) {pid_t childpid;long long start;if (server.bgsavechildpid != -1) return REDIS_ERR;if (server.vm_enabled) waitEmptyIOJobsQueue();server.dirty_before_bgsave = server.dirty;start = ustime();if ((childpid = fork()) == 0) {/* Child */if (server.vm_enabled) vmReopenSwapFile();if (server.ipfd > 0) close(server.ipfd);if (server.sofd > 0) close(server.sofd);if (rdbSave(filename) == REDIS_OK) {_exit(0);} else {_exit(1);}} else {/* Parent */casino server.stat_fork_time = ustime()-start;if (childpid == -1) {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.bgsavechildpid = childpid;updateDictResizePolicy();return REDIS_OK;}return REDIS_OK; /* unreached */ }- 對是否已經(jīng)有寫rdb的子進程進行了判斷,如果已經(jīng)有保存快照的子進程,則返回錯誤。
- 如果啟動了虛擬內(nèi)存,則等待所有處理換出換入的任務線程退出,如果還有vm任務在處理就會一直循環(huán)等待。一直到所有換入換出任務都完成且所有vm線程退出。
- 保存當前的臟數(shù)據(jù)計數(shù),當快照保存完后用于更新當前的臟數(shù)據(jù)計數(shù)(見函數(shù)backgroundSaveDoneHandler,rdb.c:1062)
- 記下當前時間,用于統(tǒng)計fork一個進程需要的時間
- Fork一個字進程,子進程調(diào)用rdbSave進行快照保存
- 父進程統(tǒng)計fork一個子進程消耗的時間: server.stat_fork_time = ustime()-start,這個統(tǒng)計可以通過info命令獲得。
- 保存子進程ID和更新增量重哈希的策略,即此時不應該再進行增量重哈希,不然大量key的改變可能導致fork的copy-on-write進行大量的寫。
到了這里我們知道,rdb的快照保存是通過函數(shù)rdbSave函數(shù)(rdb.c:394)來實現(xiàn)的。其實save命令也是通過調(diào)用這個函數(shù)來實現(xiàn)的。我們來簡單看看
Db.c:323
void saveCommand(redisClient *c) {if (server.bgsavechildpid != -1) {addReplyError(c,"Background save already in progress");return;}if (rdbSave(server.dbfilename) == REDIS_OK) {addReply(c,shared.ok);} else {addReply(c,shared.err);}最后我們進rdbSave函數(shù)看看
rdb.c:394
int rdbSave(char *filename) {.../* Wait for I/O therads to terminate, just in case this is a* foreground-saving, to avoid seeking the swap file descriptor at the* same time. */if (server.vm_enabled)waitEmptyIOJobsQueue();snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());fp = fopen(tmpfile,"w");if (!fp) {redisLog(REDIS_WARNING, "Failed saving the DB: %s", strerror(errno));return REDIS_ERR;}if (fwrite("REDIS0002",9,1,fp) == 0) 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(fp,REDIS_SELECTDB) == -1) goto werr;if (rdbSaveLen(fp,j) == -1) goto werr;/* Iterate this DB writing every entry */while((de = dictNext(di)) != NULL) {sds keystr = dictGetEntryKey(de);robj key, *o = dictGetEntryVal(de);time_t expiretime;initStaticStringObject(key,keystr);expiretime = getExpire(db,&key);/* Save the expire time */if (expiretime != -1) {/* If this key is already expired skip it */if (expiretime < now) continue;if (rdbSaveType(fp,REDIS_EXPIRETIME) == -1) goto werr;if (rdbSaveTime(fp,expiretime) == -1) goto werr;}/* Save the key and associated value. This requires special* handling if the value is swapped out. */if (!server.vm_enabled || o->storage == REDIS_VM_MEMORY ||o->storage == REDIS_VM_SWAPPING) {int otype = getObjectSaveType(o);/* Save type, key, value */if (rdbSaveType(fp,otype) == -1) goto werr;if (rdbSaveStringObject(fp,&key) == -1) goto werr;if (rdbSaveObject(fp,o) == -1) goto werr;} else {/* REDIS_VM_SWAPPED or REDIS_VM_LOADING */robj *po;/* Get a preview of the object in memory */po = vmPreviewObject(o);/* Save type, key, value */if (rdbSaveType(fp,getObjectSaveType(po)) == -1)goto werr;if (rdbSaveStringObject(fp,&key) == -1) goto werr;if (rdbSaveObject(fp,po) == -1) goto werr;/* Remove the loaded object from memory */decrRefCount(po);}}dictReleaseIterator(di);}/* EOF opcode */if (rdbSaveType(fp,REDIS_EOF) == -1) goto werr;/* Make sure data will not remain on the OS"s output buffers */fflush(fp);fsync(fileno(fp));fclose(fp);/* 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);return REDIS_OK;werr:fclose(fp);unlink(tmpfile);redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));if (di) dictReleaseIterator(di);return REDIS_ERR; }- 對是否有vm線程進行再次判斷,因為如果是通過save命令過來的是沒有判斷過vm線程的。
- 創(chuàng)建并打開臨時文件
- 寫入文件簽名“REDIS”和版本號“0002”
- 遍歷所有db中的所有key
- 對每個key,先判斷是否設置了expireTime, 如果設置了,則保存expireTime到rdb文件中。然后判斷該key對應的value是否則內(nèi)存中,如果是在內(nèi)存中,則取出來寫入到rdb文件中保存,如果被換出到虛擬內(nèi)存了,則從虛擬內(nèi)存讀取然后寫入到rdb文件中。
- 不同類型有有不同的存儲格式,詳細見rdb文件格式
- 最后寫入rdb文件的結(jié)束符
- 關閉文件并重命名臨時文件名到正式文件名
- 更新臟數(shù)據(jù)計數(shù)server.dirty為0和最近寫rdb文件的時間server.lastsave為當前時間,這個只是在通過save命令觸發(fā)的情況下有用。因為如果是通過fork一個子進程來寫rdb文件的,更新無效,因為更新的是子進程的數(shù)據(jù)。
如果是通過fork一個子進程來寫rdb文件(即不是通過save命令觸發(fā)的),在寫rdb文件的過程中,可能又有一些數(shù)據(jù)被更改了,那此時的臟數(shù)據(jù)計數(shù)server.dirty怎么更新呢? redis是怎樣處理的呢?
我們來看看寫rdb的子進程推出時得處理
Redis.c:605
if (server.bgsavechildpid != -1 || server.bgrewritechildpid != -1) {int statloc;pid_t pid;if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {if (pid == server.bgsavechildpid) {backgroundSaveDoneHandler(statloc);} else {backgroundRewriteDoneHandler(statloc);}updateDictResizePolicy();} }- 如果捕捉到寫rdb文件的子進程退出,則調(diào)用backgroundSaveDoneHandler進行處理
接著看看backgroundSaveDoneHandler函數(shù)
Rdb.c:1062
void backgroundSaveDoneHandler(int statloc) {int exitcode = WEXITSTATUS(statloc);int bysignal = WIFSIGNALED(statloc);if (!bysignal && exitcode == 0) {redisLog(REDIS_NOTICE,"Background saving terminated with success");server.dirty = server.dirty - server.dirty_before_bgsave;server.lastsave = time(NULL);} else if (!bysignal && exitcode != 0) {redisLog(REDIS_WARNING, "Background saving error");} else {redisLog(REDIS_WARNING,"Background saving terminated by signal %d", WTERMSIG(statloc));rdbRemoveTempFile(server.bgsavechildpid);}server.bgsavechildpid = -1;/* Possibly there are slaves waiting for a BGSAVE in order to be served* (the first stage of SYNC is a bulk transfer of dump.rdb) */updateSlavesWaitingBgsave(exitcode == 0 ? REDIS_OK : REDIS_ERR); }- 更新臟數(shù)據(jù)計數(shù)server.dirty為0和最近寫rdb文件的時間server.lastsave為當前時間
- 喚醒因為正在保存快照而等待的slave,關于slave的具體內(nèi)容,見replication
快照導入
當redis因為停電或者某些原因掛掉了,此時重啟redis時,我們就需要從rdb文件中讀取快照文件,把保存到rdb文件中的數(shù)據(jù)重新導入到內(nèi)存中。
先來看看啟動時對快照導入的處理
Redis.c:1717
if (server.appendonly) {if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK)redisLog(REDIS_NOTICE,"DB loaded from append only file: %ld seconds",time(NULL)-start);} else {if (rdbLoad(server.dbfilename) == REDIS_OK) {redisLog(REDIS_NOTICE,"DB loaded from disk: %ld seconds",time(NULL)-start);} else if (errno != ENOENT) {redisLog(REDIS_WARNING,"Fatal error loading the DB. Exiting.");exit(1);}}- 如果保存了AOF文件,則使用AOF文件來恢復數(shù)據(jù),AOF的具體內(nèi)容見AOF
- 如果沒有AOF,則使用rdb文件恢復數(shù)據(jù),調(diào)用rdbLoad函數(shù)
接著看看rdbLoad函數(shù)
Rdb.c:929
int rdbLoad(char *filename) {...fp = fopen(filename,"r");if (!fp) {errno = ENOENT;return REDIS_ERR;}if (fread(buf,9,1,fp) == 0) goto eoferr;buf[9] = "\0";if (memcmp(buf,"REDIS",5) != 0) {fclose(fp);redisLog(REDIS_WARNING,"Wrong signature trying to load DB from file");errno = EINVAL;return REDIS_ERR;}rdbver = atoi(buf 5);if (rdbver < 1 || rdbver > 2) {fclose(fp);redisLog(REDIS_WARNING,"Can"t handle RDB format version %d",rdbver);errno = EINVAL;return REDIS_ERR;}startLoading(fp);while(1) {robj *key, *val;int force_swapout;expiretime = -1;/* Serve the clients from time to time */if (!(loops % 1000)) {loadingProgress(ftello(fp));aeProcessEvents(server.el, AE_FILE_EVENTS|AE_DONT_WAIT);}/* Read type. */if ((type = rdbLoadType(fp)) == -1) goto eoferr;if (type == REDIS_EXPIRETIME) {if ((expiretime = rdbLoadTime(fp)) == -1) goto eoferr;/* We read the time so we need to read the object type again */if ((type = rdbLoadType(fp)) == -1) goto eoferr;}if (type == REDIS_EOF) break;/* Handle SELECT DB opcode as a special case */if (type == REDIS_SELECTDB) {if ((dbid = rdbLoadLen(fp,NULL)) == REDIS_RDB_LENERR)goto eoferr;if (dbid >= (unsigned)server.dbnum) {redisLog(REDIS_WARNING,"FATAL: Data file was created with a Redis server configured to handle more than %d databases. Exiting\n", server.dbnum);exit(1);}db = server.db dbid;continue;}/* Read key */if ((key = rdbLoadStringObject(fp)) == NULL) goto eoferr;/* Read value */if ((val = rdbLoadObject(type,fp)) == NULL) goto eoferr;/* Check if the key already expired. This function is used when loading* an RDB file from disk, either at startup, or when an RDB was* received from the master. In the latter case, the master is* responsible for key expiry. If we would expire keys here, the* snapshot taken by the master may not be reflected on the slave. */if (server.masterhost == NULL && expiretime != -1 && expiretime < now) {decrRefCount(key);decrRefCount(val);continue;}/* Add the new object in the hash table */dbAdd(db,key,val);/* Set the expire time if needed */if (expiretime != -1) setExpire(db,key,expiretime);/* Handle swapping while loading big datasets when VM is on *//* If we detecter we are hopeless about fitting something in memory* we just swap every new key on disk. Directly…* Note that"s important to check for this condition before resorting* to random sampling, otherwise we may try to swap already* swapped keys. */if (swap_all_values) {dictEntry *de = dictFind(db->dict,key->ptr);/* de may be NULL since the key already expired */if (de) {vmpointer *vp;val = dictGetEntryVal(de);if (val->refcount == 1 &&(vp = vmSwapObjectBlocking(val)) != NULL)dictGetEntryVal(de) = vp;}decrRefCount(key);continue;}decrRefCount(key);/* Flush data on disk once 32 MB of additional RAM are used… */force_swapout = 0;if ((zmalloc_used_memory() - server.vm_max_memory) > 1024*1024*32)force_swapout = 1;/* If we have still some hope of having some value fitting memory* then we try random sampling. */if (!swap_all_values && server.vm_enabled && force_swapout) {while (zmalloc_used_memory() > server.vm_max_memory) {if (vmSwapOneObjectBlocking() == REDIS_ERR) break;}if (zmalloc_used_memory() > server.vm_max_memory)swap_all_values = 1; /* We are already using too much mem */}}fclose(fp);stopLoading();return REDIS_OK;eoferr: /* unexpected end of file is handled here with a fatal exit */redisLog(REDIS_WARNING,"Short read or OOM loading DB. Unrecoverable error, aborting now.");exit(1);return REDIS_ERR; /* Just to avoid warning */ }- 打開rdb文件
- 讀取rdb文件的簽名和版本號
- 開始進入?類型 | 值 | 類型 | 值?的循環(huán)讀取,可參考rdb文件格式
- 作者還做了導入的進度條,是有人反饋說rdb文件很大時導入時要很久,但又不知道進度,所以作者就加了導入的進度條,改善用戶體驗
- 讀取類型
- 如果類型是過期時間類型REDIS_EXPIRETIME,則讀取過期時間
- 如果類型是文件結(jié)束類型REDIS_EOF,則跳出?類型 | 值 | 類型 | 值?的循環(huán)讀取
- 如果類型是選擇db類型REDIS_SELECTDB,則讀取db索引并把當前db轉(zhuǎn)成該db,然后繼續(xù)?類型 | 值 | 類型 | 值?的循環(huán)讀取。
- 如果不是以上類型,則表明該類型是數(shù)據(jù)類型,讀取作為key的字符串,即讀取字符串類型的值,然后接著讀取作為value的字符串。不同類型的編碼不一樣,根據(jù)寫入時得規(guī)則解釋讀取到的值即可
- 讀取到key和value后,判斷下該key是否過期,如果過期則丟棄,不再導入,然后繼續(xù)?類型 | 值 | 類型 | 值?的循環(huán)讀取。
- 如果讀取成功,則導入到內(nèi)存,如果有過期時間則設置過期時間
- 如果配置了虛擬內(nèi)存并且內(nèi)存的使用比虛擬內(nèi)存配置的大32M時,開始隨機的取一些數(shù)據(jù)換出到虛擬內(nèi)存中。
- 從上邊我們也可以看到,如果沒有配置虛擬內(nèi)存,rdb文件導入時會盡可能地占用操作系統(tǒng)的內(nèi)存,甚至可能全部用完。
總結(jié)
落地存儲是數(shù)據(jù)設計的一大重點也是難點。原理很簡單,定義某種協(xié)議,然后按照某種協(xié)議寫入讀出。Redis為了節(jié)省空間和讀寫時的I/O操作,做了很多很細致的工作來壓縮數(shù)據(jù)。另外redis的豐富的數(shù)據(jù)類型也加大了落地的實現(xiàn)難度。作者也曾經(jīng)在他的博客說過,redis的豐富的數(shù)據(jù)類型導致了很多經(jīng)典的優(yōu)化辦法無法在redis上實現(xiàn)。
from:?http://blog.nosqlfan.com/html/4039.html
總結(jié)
以上是生活随笔為你收集整理的深入剖析Redis RDB持久化机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解密Redis持久化
- 下一篇: Redis RDB文件格式全解析