此博客將逐步遷移到作者新的博客,可以點擊此處進入。
rdb 文件是一個經過壓縮的二進制文件,上一章講了 rdb 持久化 - 應用場景,本章主要講述 rdb 文件的結構組成包含了哪些數據。
rdb 臨時文件
redis 內存數據異步落地到臨時 rdb 文件,成功存儲後,臨時文件覆蓋原有文件。
/* flags on the purpose of rdb save or load */
#define RDBFLAGS_NONE 0
#define RDBFLAGS_AOF_PREAMBLE (1<<0)
#define RDBFLAGS_REPLICATION (1<<1)
#define REDIS_AUTOSYNC_BYTES (1024*1024*32) /* fdatasync every 32MB */
// 主進程 fork 子進程存盤
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
...
if ((childpid = redisFork()) == 0) {
...
/* Child */
retval = rdbSave(filename,rsi);
...
}
...
}
// 內存數據 -> 臨時 rdb 文件 -> 覆蓋原 rdb 文件
int rdbSave(char *filename, rdbSaveInfo *rsi) {
...
// 初始化 rdb 文件結構
rioInitWithFile(&rdb,fp);
startSaving(RDBFLAGS_NONE);
// 寫文件緩存,緩存滿 REDIS_AUTOSYNC_BYTES,緩存刷新到磁盤。
if (server.rdb_save_incremental_fsync)
rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);
// 將內存數據寫入 rio 文件
if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) {
errno = error;
goto werr;
}
/* fflush 是 libc 提供的方法,調用 write 函數寫到磁盤[其實是寫到內核的緩衝區]。
* fsync 是系統提供的系統調用,把內核緩衝刷到磁盤上。*/
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
if (rename(tmpfile,filename) == -1) {...}
...
}
逐步持久化
內存可以逐步持久化到磁盤,緩存滿 REDIS_AUTOSYNC_BYTES (32MB),緩存刷新到磁盤。這樣將大數據分散開來,減少系統壓力,避免一次寫盤帶來的問題。
# redis.conf
rdb-save-incremental-fsync yes
void rioSetAutoSync(rio *r, off_t bytes) {
if (r->write != rioFileIO.write) return;
r->io.file.autosync = bytes;
}
static size_t rioFileWrite(rio *r, const void *buf, size_t len) {
size_t retval;
retval = fwrite(buf,len,1,r->io.file.fp);
r->io.file.buffered += len;
if (r->io.file.autosync &&
r->io.file.buffered >= r->io.file.autosync)
{
fflush(r->io.file.fp);
redis_fsync(fileno(r->io.file.fp));
r->io.file.buffered = 0;
}
return retval;
}
結構
粗略將 rdb 文件的結構元素添加到圖表,可以看作是“僞代碼”吧,有些元素是建立在一定條件下才會添加進去。
有興趣的朋友,可以參考我的帖子:用 gdb 調試 redis,下個斷點,走一下 redis 保存和加載 rdb 文件的工作流程。
數據保存時序
從上圖我們可以看到 rdb 文件的結構。整個文件是由不同類型的數據單元組成的(type + value
) 。內存持久化爲 rdb 文件,我們可以參考 rdbSaveRio
。
redis 加載 rdb 文件時(
rdbLoadRio
),也是先讀出數據類型 (type
),再根據數據類型,加載對應的數據——這樣順序將 rdb 文件數據加載到內存。
/* Produces a dump of the database in RDB format sending it to the specified
* Redis I/O channel. */
int rdbSaveRio(rio *rdb, int *error, int rdbflags, rdbSaveInfo *rsi) {
...
snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION);
// 寫入 rdb 版本號
if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;
// 寫入 redis 屬性信息
if (rdbSaveInfoAuxFields(rdb,rdbflags,rsi) == -1) goto werr;
// 寫入擴展插件‘before’數據
if (rdbSaveModulesAux(rdb, REDISMODULE_AUX_BEFORE_RDB) == -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);
// 保存數據庫 id
if (rdbSaveType(rdb,RDB_OPCODE_SELECTDB) == -1) goto werr;
if (rdbSaveLen(rdb,j) == -1) goto werr;
// 保存數據庫字典大小(db->dict),過期字典大小(db->expires)。
uint64_t db_size, expires_size;
db_size = dictSize(db->dict);
expires_size = dictSize(db->expires);
if (rdbSaveType(rdb,RDB_OPCODE_RESIZEDB) == -1) goto werr;
if (rdbSaveLen(rdb,db_size) == -1) goto werr;
if (rdbSaveLen(rdb,expires_size) == -1) goto werr;
// 遍歷數據庫數據。
while((de = dictNext(di)) != NULL) {
sds keystr = dictGetKey(de);
robj key, *o = dictGetVal(de);
long long expire;
initStaticStringObject(key,keystr);
expire = getExpire(db,&key);
// 保存 key,value,expire。
if (rdbSaveKeyValuePair(rdb,&key,o,expire) == -1) goto werr;
/* When this RDB is produced as part of an AOF rewrite, move
* accumulated diff from parent to child while rewriting in
* order to have a smaller final write. */
if (rdbflags & RDBFLAGS_AOF_PREAMBLE &&
rdb->processed_bytes > processed+AOF_READ_DIFF_INTERVAL_BYTES)
{
processed = rdb->processed_bytes;
aofReadDiffFromParent();
}
}
dictReleaseIterator(di);
di = NULL; /* So that we don't release it again on error. */
}
/* If we are storing the replication information on disk, persist
* the script cache as well: on successful PSYNC after a restart, we need
* to be able to process any EVALSHA inside the replication backlog the
* master will send us. */
if (rsi && dictSize(server.lua_scripts)) {
di = dictGetIterator(server.lua_scripts);
while((de = dictNext(di)) != NULL) {
robj *body = dictGetVal(de);
if (rdbSaveAuxField(rdb,"lua",3,body->ptr,sdslen(body->ptr)) == -1)
goto werr;
}
dictReleaseIterator(di);
di = NULL; /* So that we don't release it again on error. */
}
// 寫入擴展插件‘after’數據。
if (rdbSaveModulesAux(rdb, REDISMODULE_AUX_AFTER_RDB) == -1) goto werr;
// 保存 rdb 文件結束符。
if (rdbSaveType(rdb,RDB_OPCODE_EOF) == -1) goto werr;
// 寫入 crc64 檢驗碼。
cksum = rdb->cksum;
memrev64ifbe(&cksum);
if (rioWrite(rdb,&cksum,8) == 0) goto werr;
return C_OK;
...
}
保存集羣複製信息
rdb 實現附加功能,保存服務數據複製的相關信息。當服務在某些數據複製場景下,需要 redis 進程的內存複製 id,複製位置,可以直接保存在 rdb 中,即便redis 服務重啓或者服務角色發生轉移(由主服務變成從服務),也可以從 rdb 文件中,獲得相應的複製數據信息,不至於什麼信息都沒有,需要重新全量同步。
可以參考 redis 這兩個源碼改動:PSYNC2: Save replication ID/offset on RDB file.,PSYNC2: different improvements to Redis replication.
/* This structure can be optionally passed to RDB save/load functions in
* order to implement additional functionalities, by storing and loading
* metadata to the RDB file.
*
* Currently the only use is to select a DB at load time, useful in
* replication in order to make sure that chained slaves (slaves of slaves)
* select the correct DB and are able to accept the stream coming from the
* top-level master. */
typedef struct rdbSaveInfo {
/* Used saving and loading. */
int repl_stream_db; /* DB to select in server.master client. */
/* Used only loading. */
int repl_id_is_set; /* True if repl_id field is set. */
char repl_id[CONFIG_RUN_ID_SIZE+1]; /* Replication ID. */
long long repl_offset; /* Replication offset. */
} rdbSaveInfo;
// 保存複製副本相關信息。
int rdbSaveInfoAuxFields(rio *rdb, int rdbflags, rdbSaveInfo *rsi) {
...
if (rsi) {
if (rdbSaveAuxFieldStrInt(rdb,"repl-stream-db",rsi->repl_stream_db)
== -1) return -1;
if (rdbSaveAuxFieldStrStr(rdb,"repl-id",server.replid)
== -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"repl-offset",server.master_repl_offset)
== -1) return -1;
}
...
}
保存屬性信息
// 寫入 redis 屬性信息
int rdbSaveInfoAuxFields(rio *rdb, int rdbflags, rdbSaveInfo *rsi) {
int redis_bits = (sizeof(void*) == 8) ? 64 : 32;
int aof_preamble = (rdbflags & RDBFLAGS_AOF_PREAMBLE) != 0;
/* Add a few fields about the state when the RDB was created. */
// 寫入 redis 版本號
if (rdbSaveAuxFieldStrStr(rdb,"redis-ver",REDIS_VERSION) == -1) return -1;
// 寫入redis 工作的機器多少位。
if (rdbSaveAuxFieldStrInt(rdb,"redis-bits",redis_bits) == -1) return -1;
// rdb 寫入數據時間
if (rdbSaveAuxFieldStrInt(rdb,"ctime",time(NULL)) == -1) return -1;
// 當前使用內存大小
if (rdbSaveAuxFieldStrInt(rdb,"used-mem",zmalloc_used_memory()) == -1) return -1;
// 存儲從庫信息,方便 (slaves of slaves) 數據同步
if (rsi) {
if (rdbSaveAuxFieldStrInt(rdb,"repl-stream-db",rsi->repl_stream_db)
== -1) return -1;
if (rdbSaveAuxFieldStrStr(rdb,"repl-id",server.replid)
== -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"repl-offset",server.master_repl_offset)
== -1) return -1;
}
if (rdbSaveAuxFieldStrInt(rdb,"aof-preamble",aof_preamble) == -1) return -1;
return 1;
}
保存 key-value
#define RDB_OPCODE_IDLE 248 /* LRU idle time. */
#define RDB_OPCODE_FREQ 249 /* LFU frequency. */
#define RDB_OPCODE_AUX 250 /* RDB aux field. */
#define RDB_OPCODE_EXPIRETIME_MS 252 /* Expire time in milliseconds. */
/* Save a key-value pair, with expire time, type, key, value.
* On error -1 is returned.
* On success if the key was actually saved 1 is returned, otherwise 0
* is returned (the key was already expired). */
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime) {
int savelru = server.maxmemory_policy & MAXMEMORY_FLAG_LRU;
int savelfu = server.maxmemory_policy & MAXMEMORY_FLAG_LFU;
// 保存數據到期時間。
if (expiretime != -1) {
if (rdbSaveType(rdb,RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
}
// 保存數據 lru 時間,精度是秒,這樣可以減少存儲的空間。
if (savelru) {
uint64_t idletime = estimateObjectIdleTime(val);
idletime /= 1000; /* Using seconds is enough and requires less space.*/
if (rdbSaveType(rdb,RDB_OPCODE_IDLE) == -1) return -1;
if (rdbSaveLen(rdb,idletime) == -1) return -1;
}
// 保存數據使用頻率信息。
if (savelfu) {
uint8_t buf[1];
buf[0] = LFUDecrAndReturn(val);
// 使用頻率是一個 0 - 255 的計數,只用一個字節保存即可。
if (rdbSaveType(rdb,RDB_OPCODE_FREQ) == -1) return -1;
if (rdbWriteRaw(rdb,buf,1) == -1) return -1;
}
// 保存數據類型。
if (rdbSaveObjectType(rdb,val) == -1) return -1;
// 保存鍵數據。
if (rdbSaveStringObject(rdb,key) == -1) return -1;
// 保存鍵對應數據信息。
if (rdbSaveObject(rdb,val,key) == -1) return -1;
...
return 1;
}
// 數據對象,根據不同的結構類型,進行保存。
ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key) {
...
if (o->type == OBJ_STRING) {
...
} else if (o->type == OBJ_LIST) {
...
} else if (o->type == OBJ_SET) {
...
} else if (o->type == OBJ_ZSET) {
...
} else if (o->type == OBJ_HASH) {
...
} else if (o->type == OBJ_STREAM) {
...
} else if (o->type == OBJ_MODULE) {
...
}
...
}