[redis 源碼走讀] rdb 持久化 - 文件結構

此博客將逐步遷移到作者新的博客,可以點擊此處進入。

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 文件的結構元素添加到圖表,可以看作是“僞代碼”吧,有些元素是建立在一定條件下才會添加進去。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) {
        ...
    }
    ...
}

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章