本文主要說明Redis的RDB持久化方式的實現(xiàn)及運作機制歹茶。
推薦閱讀:
1亏娜、Redis 的RDB持久化方式的理論說明見: Redis之RDB持久化小探
I腊瑟、核心數(shù)據(jù)結構rio
持久化的IO操作在rio.h
與rio.c
中實現(xiàn)音瓷,核心數(shù)據(jù)結構是struct rio
般甲。RDB中的幾乎每個函數(shù)都帶有rio
參數(shù),其抽象了文件和內存的操作:
/*RIO API 接口和狀態(tài)*/
/*src/rio.h/_rio*/
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. */
// API
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. */
// 校驗和計算函數(shù)俊马,每次有寫入/讀取新數(shù)據(jù)時都要計算一次
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可以是緩存丁存,也可以是一個文件IO
* 因此在RDB持久化的時候可以將RDB保存到磁盤文件,也可以保存在內存中柴我,但保存在內存中其實就不是持久化了解寝。
*/
union {
struct {
// 緩存指針
sds ptr;
// 偏移量
off_t pos;
} buffer;
struct {
// 被打開文件的指針
FILE *fp;
// 最近一次 fsync() 以來,寫入的字節(jié)量
off_t buffered; /* Bytes written since last fsync. */
// 寫入多少字節(jié)之后艘儒,才會自動執(zhí)行一次 fsync()
off_t autosync; /* fsync after 'autosync' bytes written. */
} file;
} io;
};
typedef struct _rio rio;
下面兩個數(shù)據(jù)結構分別表示流為內存與流為文件:
// 適用于內存緩存
/*src/rio.c/rioBufferIO*/
static const rio rioBufferIO = {
rioBufferRead, //讀函數(shù)
rioBufferWrite, //寫函數(shù)
rioBufferTell, //偏移量函數(shù)
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 */
};
II聋伦、RDB持久化機制
RDB有兩種持久化機制,一個是save
界睁,另一個是bgsave
這兩個的區(qū)別在于是否阻塞客戶端服務觉增,因為bgsave
是fork
子進程的方式完成的。這里重點說明以子進程的方式完成持久化:
通過調用rdbSaveBackground()
函數(shù)完成子進程的fork
:
/*bgsave 主函數(shù)*/
/*src/rdb.c/rdbSaveBackground*/
int rdbSaveBackground(char *filename) {
pid_t childpid;
long long start;
// 如果 BGSAVE 已經在執(zhí)行翻斟,那么出錯
if (server.rdb_child_pid != -1) return REDIS_ERR;
// 記錄 BGSAVE 執(zhí)行前的數(shù)據(jù)庫被修改次數(shù)
server.dirty_before_bgsave = server.dirty;
// 最近一次嘗試執(zhí)行 BGSAVE 的時間
server.lastbgsave_try = time(NULL);
// fork() 開始前的時間逾礁,記錄 fork() 返回耗時用
start = ustime();
if ((childpid = fork()) == 0) {
int retval;
/* Child */
// 關閉網絡連接 fd
closeListeningSockets(0);
// 設置進程的標題,方便識別
redisSetProcTitle("redis-rdb-bgsave");
// 調用rdbSave函數(shù)访惜,真正執(zhí)行保存操作
retval = rdbSave(filename);
// 打印 copy-on-write 時使用的內存數(shù)
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));
}
}
// 向父進程發(fā)送信號
exitFromChild((retval == REDIS_OK) ? 0 : 1);
} else {
/* Parent */
// 計算 fork() 執(zhí)行的時間
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;
}
// 打印 BGSAVE 開始的日志
redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
// 記錄數(shù)據(jù)庫開始 BGSAVE 的時間
server.rdb_save_time_start = time(NULL);
// 記錄負責執(zhí)行 BGSAVE 的子進程 ID
server.rdb_child_pid = childpid;
// 關閉自動 rehash
updateDictResizePolicy();
return REDIS_OK;
}
return REDIS_OK; /* unreached */
}
可以看出,其中的主要調用為rdbSave
函數(shù)债热,這也是阻塞式save
的底層函數(shù):
/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success
* 將數(shù)據(jù)庫保存到磁盤上砾嫉。
*/
/*src/rdb.c/rdbSave*/
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;
// 創(chuàng)建臨時文件
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,這里是創(chuàng)建了rdb的結構體窒篱。
rioInitWithFile(&rdb,fp);
// 設置校驗和函數(shù)
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;
// 遍歷所有數(shù)據(jù)庫
for (j = 0; j < server.dbnum; j++) {
// 指向數(shù)據(jù)庫
redisDb *db = server.db+j;
// 指向數(shù)據(jù)庫鍵空間
dict *d = db->dict;
// 跳過空數(shù)據(jù)庫
if (dictSize(d) == 0) continue;
// 創(chuàng)建鍵空間迭代器
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
*
* 遍歷數(shù)據(jù)庫焰枢,并寫入每個鍵值對的數(shù)據(jù)
*/
while((de = dictNext(di)) != NULL) {
sds keystr = dictGetKey(de);
robj key, *o = dictGetVal(de);
long long expire;
// 根據(jù) keystr 蚓峦,在棧中創(chuàng)建一個 key 對象
initStaticStringObject(key,keystr);
// 獲取鍵的過期時間
expire = getExpire(db,&key);
// 保存鍵值對數(shù)據(jù)
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 */
// 沖洗緩存一汽,確保數(shù)據(jù)已寫入磁盤
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");
// 清零數(shù)據(jù)庫臟狀態(tài)
server.dirty = 0;
// 記錄最后一次完成 SAVE 的時間
server.lastsave = time(NULL);
// 記錄最后一次執(zhí)行 SAVE 的狀態(tài)
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;
}
這里需要說明幾點:
1恕沫、采用BGSAVE
策略监憎,如果內存中的數(shù)據(jù)集很大,fork
會因為要為子進程產生一份虛擬空間(讀時共享婶溯,寫時拷貝)而花費的時間很長鲸阔;可能會造成阻塞。
2迄委、在RDB持久化過程中褐筛,每一個數(shù)據(jù)都需要調用一個write
的系統(tǒng)調用,CPU資源可能會緊張叙身,因此需要避免在一臺物理機上部署多個Redis渔扎,避免同時持久化。
3信轿、在子進程完成之前晃痴,讀取了自身的私有臟數(shù)據(jù)private_dirty
的大小,這可以近似看成是bgsave
進行過程中占用了多少內存财忽。
III倘核、RDB文件
在rdbSave
函數(shù)中,對于每一個鍵值對定罢,都會調用函數(shù)rdbSaveKeyValuePair
函數(shù)進行存儲,我們可以了解RDB文件對于每個鍵值對的組織形式旁瘫,在看是如何存儲的:
再來理解rdbSaveKeyValuePair
函數(shù)的實現(xiàn):
/* Save a key-value pair, with expire time, type, key, value.
*
* 將鍵值對的鍵祖凫、值、過期時間和類型寫入到 RDB 中酬凳。
*
* On success if the key was actually saved 1 is returned, otherwise 0
* is returned (the key was already expired).
*
* 成功保存返回 1 惠况,當鍵已經過期時,返回 0 宁仔。
*/
/*src/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;
}
【參考】
[1] 《Redis設計與實現(xiàn)》
[2] 《Redis源碼日志》