redis 提供兩種持久化方式:RDB 和 AOF。redis 容許二者結合,也容許二者同時關閉。git
本篇主要講的是 RDB 持久化,瞭解 RDB 的數據保存結構和運做機制。redis 主要在 rdb.h 和 rdb.c 兩個文件中實現 RDB 的操做。github
持久化的 IO 操做在 rio.h 和 rio.c 中實現,核心數據結構是 struct rio。RDB 中的幾乎每個函數都帶有 rio 參數。struct rio 既適用於文件,又適用於內存緩存,從 struct rio 的實現可見一斑。redis
struct _rio { // 函數指針,包括讀操做,寫操做和文件指針移動操做 /* Backend functions. * Since this functions do not tolerate short writes or reads the return * value is simplified to: zero on error, non zero on complete success. */ size_t (*read)(struct _rio *, void *buf, size_t len); size_t (*write)(struct _rio *, const void *buf, size_t len); off_t (*tell)(struct _rio *); // 校驗和計算函數 /* The update_cksum method if not NULL is used to compute the checksum of * all the data that was read or written so far. The method should be * designed so that can be called with the current checksum, and the buf * and len fields pointing to the new block of data to add to the checksum * computation. */ void (*update_cksum)(struct _rio *, const void *buf, size_t len); // 校驗和 /* The current checksum */ uint64_t cksum; // 已經讀取或者寫入的字符數 /* number of bytes read or written */ size_t processed_bytes; // 每次最多能處理的字符數 /* maximum single read or write chunk size */ size_t max_processing_chunk; // 能夠是一個內存總的字符串,也能夠是一個文件描述符 /* Backend-specific vars. */ union { struct { sds ptr; // 偏移量 off_t pos; } buffer; struct { FILE *fp; // 偏移量 off_t buffered; /* Bytes written since last fsync. */ off_t autosync; /* fsync after 'autosync' bytes written. */ } file; } io; }; typedef struct _rio rio;
redis 定義兩個 struct rio,分別是 rioFileIO 和 rioBufferIO,前者用於內存緩存,後者用於文件 IO:數據庫
// 適用於內存緩存 static const rio rioBufferIO = { rioBufferRead, rioBufferWrite, rioBufferTell, NULL, /* update_checksum */ 0, /* current checksum */ 0, /* bytes read or written */ 0, /* read/write chunk size */ { { NULL, 0 } } /* union for io-specific vars */ }; // 適用於文件 IO 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 */ };
redis 支持兩種方式進行 RDB:當前進程執行和後臺執行(BGSAVE)。RDB BGSAVE 策略是 fork 出一個子進程,把內存中的數據集整個 dump 到硬盤上。兩個場景舉例:緩存
這裏主要展開的內容是 RDB 持久化操做的寫文件過程,讀過程和寫過程相反。子進程的產生髮生在 rdbSaveBackground() 中,真正的 RDB 持久化操做是在 rdbSave(),想要直接進行 RDB 持久化,調用 rdbSave() 便可。服務器
如下主要以代碼的方式來展開 RDB 的運做機制:數據結構
// 備份主程序 /* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success */ 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; } // 初始化 rdb 結構體。rdb 結構體內指定了讀寫文件的函數,已寫/讀字符統計等數據 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++) { // server 中保存的數據 redisDb *db = server.db+j; // 字典 dict *d = db->dict; if (dictSize(d) == 0) continue; // 字典迭代器 di = dictGetSafeIterator(d); if (!di) { fclose(fp); return REDIS_ERR; } // 寫入 RDB 操做碼 /* 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; // 將 keystr 封裝在 robj 裏 initStaticStringObject(key,keystr); // 獲取過時時間 expire = getExpire(db,&key); // 開始寫入磁盤 if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr; } dictReleaseIterator(di); } di = NULL; /* So that we don't release it again on error. */ // RDB 結束碼 /* 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); rioWrite(&rdb,&cksum,8); // 同步到磁盤 /* 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); // 記錄執行的結果狀態爲成功 server.lastbgsave_status = REDIS_OK; 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; } // bgsaveCommand(),serverCron(),syncCommand(),updateSlavesWaitingBgsave() 會調用 rdbSaveBackground() int rdbSaveBackground(char *filename) { pid_t childpid; long long start; // 已經有後臺程序了,拒絕再次執行 if (server.rdb_child_pid != -1) return REDIS_ERR; server.dirty_before_bgsave = server.dirty; // 記錄此次嘗試執行持久化操做的時間 server.lastbgsave_try = time(NULL); start = ustime(); if ((childpid = fork()) == 0) { int retval; // 取消監聽 /* Child */ closeListeningSockets(0); redisSetProcTitle("redis-rdb-bgsave"); // 執行備份主程序 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 */ // 計算 fork 消耗的時間 server.stat_fork_time = ustime()-start; // fork 出錯 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); // 子進程 ID server.rdb_child_pid = childpid; updateDictResizePolicy(); return REDIS_OK; } return REDIS_OK; /* unreached */ }
若是採用 BGSAVE 策略,且內存中的數據集很大,fork() 會由於要爲子進程產生一份虛擬空間表而花費較長的時間;若是此時客戶端請求數量很是大的話,會致使較多的寫時拷貝操做;在 RDB 持久化操做過程當中,每個數據都會致使 write() 系統調用,CPU 資源很緊張。所以,若是在一臺物理機上部署多個 redis,應該避免同時持久化操做。函數
那如何知道 BGSAVE 佔用了多少內存?子進程在結束以前,讀取了自身私有髒數據 Private_Dirty 的大小,這樣作是爲了讓用戶看到 redis 的持久化進程所佔用了有多少的空間。在父進程 fork 產生子進程事後,父子進程雖然有不一樣的虛擬空間,但物理空間上是共存的,直至父進程或者子進程修改內存數據爲止,因此髒數據 Private_Dirty 能夠近似的認爲是子進程,即持久化進程佔用的空間。ui
RDB 的文件組織方式爲:數據集序號1:操做碼:數據1:結束碼:校驗和----數據集序號2:操做碼:數據2:結束碼:校驗和......this
其中,數據的組織方式爲:過時時間:數據類型:鍵:值,即 TVL(type,length,value)。
舉兩個字符串存儲的例子,其餘的大概都以致於的形式來組織數據:
可見,RDB 持久化的結果是一個很是緊湊的文件,幾乎每一位都是有用的信息。若是對 redis RDB 數據組織方式的細則感興趣,能夠參看 rdb.h 和 rdb.c 兩個文件的實現。
對於每個鍵值對都會調用 rdbSaveKeyValuePair(),以下:
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime, long long now) { // 過時時間 /* Save the expire time */ if (expiretime != -1) { /* If this key is already expired skip it */ if (expiretime < now) return 0; if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1; if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1; } /* Save type, key, value */ // 數據類型 if (rdbSaveObjectType(rdb,val) == -1) return -1; // 鍵 if (rdbSaveStringObject(rdb,key) == -1) return -1; // 值 if (rdbSaveObject(rdb,val) == -1) return -1; return 1; }
若是對 redis RDB 數據格式細則感興趣,歡迎訪問個人 github & 歡迎討論。
http://redis.io/topics/persistence
----
搗亂 2014-3-26