為防止數(shù)據(jù)丟失,需要將 Redis 中的數(shù)據(jù)從內(nèi)存中 dump 到磁盤(pán),這就是持久化。Redis 提供兩種持久化方式:RDB 和 AOF。Redis 允許兩者結(jié)合,也允許兩者同時(shí)關(guān)閉。
RDB 可以定時(shí)備份內(nèi)存中的數(shù)據(jù)集。服務(wù)器啟動(dòng)的時(shí)候,可以從 RDB 文件中恢復(fù)數(shù)據(jù)集。
AOF(append only file) 可以記錄服務(wù)器的所有寫(xiě)操作。在服務(wù)器重新啟動(dòng)的時(shí)候,會(huì)把所有的寫(xiě)操作重新執(zhí)行一遍,從而實(shí)現(xiàn)數(shù)據(jù)備份。當(dāng)寫(xiě)操作集過(guò)大(比原有的數(shù)據(jù)集還大),Redis 會(huì)重寫(xiě)寫(xiě)操作集。
為什么稱為 append only file 呢?AOF 持久化是類似于生成一個(gè)關(guān)于 Redis 寫(xiě)操作的文件,寫(xiě)操作(增刪)總是以追加的方式追加到文件中。
本篇主要講的是 RDB 持久化,了解 RDB 的數(shù)據(jù)保存結(jié)構(gòu)和運(yùn)作機(jī)制。Redis 主要在 rdb.h 和 rdb.c 兩個(gè)文件中實(shí)現(xiàn) RDB 的操作。
持久化的 IO 操作在 rio.h 和 rio.c 中實(shí)現(xiàn),核心數(shù)據(jù)結(jié)構(gòu)是 struct rio。RDB 中的幾乎每一個(gè)函數(shù)都帶有 rio 參數(shù)。struct rio 既適用于文件,又適用于內(nèi)存緩存,從 struct rio 的實(shí)現(xiàn)可見(jiàn)一斑,它抽象了文件和內(nèi)存的操作。
struct _rio {
// 函數(shù)指針,包括讀操作,寫(xiě)操作和文件指針移動(dòng)操作
/* 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 *);
// 校驗(yàn)和計(jì)算函數(shù)
/* 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);
// 校驗(yàn)和
/* The current checksum */
uint64_t cksum;
// 已經(jīng)讀取或者寫(xiě)入的字符數(shù)
/* number of bytes read or written */
size_t processed_bytes;
// 每次最多能處理的字符數(shù)
/* maximum single read or write chunk size */
size_t max_processing_chunk;
// 可以是一個(gè)內(nèi)存總的字符串,也可以是一個(gè)文件描述符
/* 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 定義兩個(gè) struct rio,分別是 rioFileIO 和 rioBufferIO,前者用于內(nèi)存緩存,后者用于文件 IO:
// 適用于內(nèi)存緩存
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 */
};
因此,在 RDB 持久化的時(shí)候可以將 RDB 保存到磁盤(pán)中,也可以保存到內(nèi)存中,當(dāng)然保存到內(nèi)存中就不是持久化了。
http://wiki.jikexueyuan.com/project/redis/images/redis16.png" alt="" />
Redis 支持兩種方式進(jìn)行 RDB 持久化:當(dāng)前進(jìn)程執(zhí)行和后臺(tái)執(zhí)行(BGSAVE)。RDB BGSAVE 策略是 fork 出一個(gè)子進(jìn)程,把內(nèi)存中的數(shù)據(jù)集整個(gè) dump 到硬盤(pán)上。兩個(gè)場(chǎng)景舉例:
這里主要展開(kāi)的內(nèi)容是 RDB 持久化操作的寫(xiě)文件過(guò)程,讀過(guò)程和寫(xiě)過(guò)程相反。子進(jìn)程的產(chǎn)生發(fā)生在 rdbSaveBackground() 中,真正的 RDB 持久化操作是在 rdbSave(),想要直接進(jìn)行 RDB 持久化,調(diào)用 rdbSave() 即可。
以下主要以代碼的方式來(lái)展開(kāi) RDB 的運(yùn)作機(jī)制:
// 備份主程序
/* 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;
// 打開(kāi)文件,準(zhǔn)備寫(xiě)
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 結(jié)構(gòu)體。rdb 結(jié)構(gòu)體內(nèi)指定了讀寫(xiě)文件的函數(shù),已寫(xiě)/讀字符統(tǒng)計(jì)等數(shù)據(jù)
rioInitWithFile(&rdb,fp);
if (server.rdb_checksum) // 校驗(yàn)和
rdb.update_cksum = rioGenericUpdateChecksum;
// 先寫(xiě)入版本號(hào)
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 中保存的數(shù)據(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;
}
// 寫(xiě)入RDB 操作碼
/* Write the SELECT DB opcode */
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;
// 寫(xiě)入數(shù)據(jù)庫(kù)序號(hào)
if (rdbSaveLen(&rdb,j) == -1) goto werr;
// 寫(xiě)入數(shù)據(jù)庫(kù)中每一個(gè)數(shù)據(jù)項(xiàng)
/* 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);
// 獲取過(guò)期時(shí)間
expire = getExpire(db,&key);
// 開(kāi)始寫(xiě)入磁盤(pán)
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 結(jié)束碼
/* EOF opcode */
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
// 校驗(yàn)和
/* 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);
// 同步到磁盤(pán)
/* Make sure data will not remain on the OS's output buffers */
fflush(fp);
fsync(fileno(fp));
fclose(fp);
// 修改臨時(shí)文件名為指定文件名
/* 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;
// 記錄成功執(zhí)行保存的時(shí)間
server.lastsave = time(NULL);
// 記錄執(zhí)行的結(jié)果狀態(tài)為成功
server.lastbgsave_status = REDIS_OK;
return REDIS_OK;
werr:
// 清理工作,關(guān)閉文件描述符等
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() 會(huì)調(diào)用
// rdbSaveBackground()
int rdbSaveBackground(char *filename) {
pid_t childpid;
long long start;
// 已經(jīng)有后臺(tái)程序了,拒絕再次執(zhí)行
if (server.rdb_child_pid != -1) return REDIS_ERR;
server.dirty_before_bgsave = server.dirty;
// 記錄這次嘗試執(zhí)行持久化操作的時(shí)間
server.lastbgsave_try = time(NULL);
start = ustime();
if ((childpid = fork()) == 0) {
int retval;
// 取消監(jiān)聽(tīng)
/* Child */
closeListeningSockets(0);
redisSetProcTitle("redis-rdb-bgsave");
// 執(zhí)行備份主程序
retval = rdbSave(filename);
// 臟數(shù)據(jù),其實(shí)就是子進(jìn)程所消耗的內(nèi)存大小
if (retval == REDIS_OK) {
// 獲取臟數(shù)據(jù)大小
size_t private_dirty = zmalloc_get_private_dirty();
// 記錄臟數(shù)據(jù)
if (private_dirty) {
redisLog(REDIS_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
}
// 退出子進(jìn)程
exitFromChild((retval == REDIS_OK) ? 0 : 1);
} else {
/* Parent */
// 計(jì)算fork 消耗的時(shí)間
server.stat_fork_time = ustime()-start;
// fork 出錯(cuò)
if (childpid == -1) {
// 記錄執(zhí)行的結(jié)果狀態(tài)為失敗
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);
// 記錄保存的起始時(shí)間
server.rdb_save_time_start = time(NULL);
// 子進(jìn)程ID
server.rdb_child_pid = childpid;
updateDictResizePolicy();
return REDIS_OK;
}
return REDIS_OK; /* unreached */
}
如果采用 BGSAVE 策略,且內(nèi)存中的數(shù)據(jù)集很大,fork() 會(huì)因?yàn)橐獮樽舆M(jìn)程產(chǎn)生一份虛擬空間表而花費(fèi)較長(zhǎng)的時(shí)間;如果此時(shí)客戶端請(qǐng)求數(shù)量非常大的話,會(huì)導(dǎo)致較多的寫(xiě)時(shí)拷貝操作;在 RDB 持久化操作過(guò)程中,每一個(gè)數(shù)據(jù)都會(huì)導(dǎo)致 write() 系統(tǒng)調(diào)用,CPU 資源很緊張。因此,如果在一臺(tái)物理機(jī)上部署多個(gè) Redis,應(yīng)該避免同時(shí)持久化操作。
那如何知道 BGSAVE 占用了多少內(nèi)存?子進(jìn)程在結(jié)束之前,讀取了自身私有臟數(shù)據(jù) Private_Dirty 的大小,這樣做是為了讓用戶看到 Redis 的持久化進(jìn)程所占用了有多少的空間。在父進(jìn)程 fork 產(chǎn)生子進(jìn)程過(guò)后,父子進(jìn)程雖然有不同的虛擬空間,但物理空間上是共存的,直至父進(jìn)程或者子進(jìn)程修改內(nèi)存數(shù)據(jù)為止,所以臟數(shù) Private_Dirty 可以近似的認(rèn)為是子進(jìn)程,即持久化進(jìn)程占用的空間。
RDB 的文件組織方式為:數(shù)據(jù)集序號(hào)1:操作碼:數(shù)據(jù)1:結(jié)束碼:校驗(yàn)和—-數(shù)據(jù)集序號(hào)2:操作碼:數(shù)據(jù)2:結(jié)束碼:校驗(yàn)和……
其中,數(shù)據(jù)的組織方式為:過(guò)期時(shí)間:數(shù)據(jù)類型:鍵:值,即 TVL(type,length,value)。
舉兩個(gè)字符串存儲(chǔ)的例子,其他的大概都以至于的形式來(lái)組織數(shù)據(jù):
http://wiki.jikexueyuan.com/project/redis/images/redis17.png" alt="" />
可見(jiàn),RDB 持久化的結(jié)果是一個(gè)非常緊湊的文件,幾乎每一位都是有用的信息。如果對(duì) redis RDB 數(shù)據(jù)組織方式的細(xì)則感興趣,可以參看 rdb.h 和 rdb.c 兩個(gè)文件的實(shí)現(xiàn)。
對(duì)于每一個(gè)鍵值對(duì)都會(huì)調(diào)用 rdbSaveKeyValuePair(),如下:
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
long long expiretime, long long now)
{
// 過(guò)期時(shí)間
/* 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 */
// 數(shù)據(jù)類型
if (rdbSaveObjectType(rdb,val) == -1) return -1;
// 鍵
if (rdbSaveStringObject(rdb,key) == -1) return -1;
// 值
if (rdbSaveObject(rdb,val) == -1) return -1;
return 1;
}
在 rdb.h, rdb.c 中有關(guān)于每種數(shù)據(jù)的編碼和解碼的代碼,感興趣的同學(xué)可以詳細(xì)研讀一下。