標籤:
redis是個記憶體資料庫,所有的操作都是在記憶體中進行,但是記憶體有個特點是,程式出問題或者系統出問題、重啟,關機都會造成記憶體資料丟失。
所以需要把記憶體中的資料dump到硬碟中備份起來。
RDB持久化,是記憶體資料庫dump到硬碟的過程,其中RDB是個檔案格式,待會介紹。
本文從兩個方向剖析,
1)載入dump.rdb檔案到記憶體中。
2)記憶體資料庫dump到硬碟中dump.rdb檔案。
載入dump.rdb檔案到記憶體
main函數入口:
int main(int argc, char **argv) { //... // 從 AOF 檔案或者 RDB 檔案中載入資料 loadDataFromDisk(); //...}
loadDataFromDisk函數就是載入硬碟資料到記憶體(AOF或者RDB),具體的實現來看代碼:
/* Function called at startup to load RDB or AOF file in memory. */void loadDataFromDisk(void) { // 記錄開始時間 long long start = ustime(); // AOF 持久化已開啟? if (server.aof_state == REDIS_AOF_ON) { // 嘗試載入 AOF 檔案 if (loadAppendOnlyFile(server.aof_filename) == REDIS_OK) // 列印載入資訊,並計算載入耗時間長度度 redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000); // AOF 持久化未開啟 } else { // 嘗試載入 RDB 檔案 if (rdbLoad(server.rdb_filename) == REDIS_OK) { // 列印載入資訊,並計算載入耗時間長度度 redisLog(REDIS_NOTICE,"DB loaded from disk: %.3f seconds", (float)(ustime()-start)/1000000); } else if (errno != ENOENT) { redisLog(REDIS_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno)); exit(1); } }}
AOF下一篇檔案再剖析他的結構,現在就看RDB檔案的載入:
if (rdbLoad(server.rdb_filename) == REDIS_OK) {// RDB檔案的載入,server.rdb_filename預設值位dump.rdb
/* * 將給定 rdb 中儲存的資料載入到資料庫中。 */int rdbLoad(char *filename) { uint32_t dbid; int type, rdbver; redisDb *db = server.db+0; char buf[1024]; long long expiretime, now = mstime(); FILE *fp; rio rdb; // 開啟 rdb 檔案 if ((fp = fopen(filename,"r")) == NULL) return REDIS_ERR; // 初始化寫入流 rioInitWithFile(&rdb,fp); rdb.update_cksum = rdbLoadProgressCallback; rdb.max_processing_chunk = server.loading_process_events_interval_bytes; if (rioRead(&rdb,buf,9) == 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 > REDIS_RDB_VERSION) { 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; expiretime = -1; /* Read type. * * 讀入類型指示,決定該如何讀入之後跟著的資料。 * * 這個指示可以是 rdb.h 中定義的所有以 * REDIS_RDB_TYPE_* 為首碼的常量的其中一個 * 或者所有以 REDIS_RDB_OPCODE_* 為首碼的常量的其中一個 */ if ((type = rdbLoadType(&rdb)) == -1) goto eoferr; // 讀入到期時間值 if (type == REDIS_RDB_OPCODE_EXPIRETIME) { // 以秒計算的到期時間 if ((expiretime = rdbLoadTime(&rdb)) == -1) goto eoferr; /* We read the time so we need to read the object type again. * * 在到期時間之後會跟著一個索引值對,我們要讀入這個索引值對的類型 */ if ((type = rdbLoadType(&rdb)) == -1) goto eoferr; /* the EXPIRETIME opcode specifies time in seconds, so convert * into milliseconds. * * 將格式轉換為毫秒*/ expiretime *= 1000; } else if (type == REDIS_RDB_OPCODE_EXPIRETIME_MS) { // 以毫秒計算的到期時間 /* Milliseconds precision expire times introduced with RDB * version 3. */ if ((expiretime = rdbLoadMillisecondTime(&rdb)) == -1) goto eoferr; /* We read the time so we need to read the object type again. * * 在到期時間之後會跟著一個索引值對,我們要讀入這個索引值對的類型 */ if ((type = rdbLoadType(&rdb)) == -1) goto eoferr; } // 讀入資料 EOF (不是 rdb 檔案的 EOF) if (type == REDIS_RDB_OPCODE_EOF) break; /* Handle SELECT DB opcode as a special case * * 讀入切換資料庫指示 */ if (type == REDIS_RDB_OPCODE_SELECTDB) { // 讀入資料庫號碼 if ((dbid = rdbLoadLen(&rdb,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(&rdb)) == NULL) goto eoferr; /* Read value * * 讀入值 */ if ((val = rdbLoadObject(type,&rdb)) == 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); decrRefCount(key); } /* Verify the checksum if RDB version is >= 5 * * 如果 RDB 版本 >= 5 ,那麼比對校正和 */ if (rdbver >= 5 && server.rdb_checksum) { uint64_t cksum, expected = rdb.cksum; // 讀入檔案的校正和 if (rioRead(&rdb,&cksum,8) == 0) goto eoferr; memrev64ifbe(&cksum); // 比對校正和 if (cksum == 0) { redisLog(REDIS_WARNING,"RDB file was saved with checksum disabled: no check performed."); } else if (cksum != expected) { redisLog(REDIS_WARNING,"Wrong RDB checksum. Aborting now."); exit(1); } } // 關閉 RDB 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 */}
函數int rdbLoad(char *filename) 中,很明顯看出rdb的檔案結構,如下:
記憶體資料庫dump到硬碟中dump.rdb檔案
使用函數rdbSave:
/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success * * 將資料庫儲存到磁碟上。 * * 儲存成功返回 REDIS_OK ,出錯/失敗返回 REDIS_ERR 。 */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; } // 初始化 I/O rioInitWithFile(&rdb,fp); // 設定校正和函數 if (server.rdb_checksum) rdb.update_cksum = rioGenericUpdateChecksum; // 寫入 RDB 版本號碼 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 * * 寫入 DB 選取器 */ 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 ,在棧中建立一個 key 對象 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. */ /* EOF opcode * * 寫入 EOF 代碼 */ 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. * * CRC64 校正和。 * * 如果校正和功能已關閉,那麼 rdb.cksum 將為 0 , * 在這種情況下, RDB 載入時會跳過校正和檢查。 */ cksum = rdb.cksum; memrev64ifbe(&cksum); rioWrite(&rdb,&cksum,8); /* 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. * * 使用 RENAME ,原子性地對臨時檔案進行改名,覆蓋原來的 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; } // 寫入完成,列印日誌 redisLog(REDIS_NOTICE,"DB saved on disk"); // 清零資料庫髒狀態 server.dirty = 0; // 記錄最後一次完成 SAVE 的時間 server.lastsave = time(NULL); // 記錄最後一次執行 SAVE 的狀態 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;}
以上是大致rdb持最佳化的過程,細節還得繼續扣代碼。
redis 源碼學習(RDB 持久化)