Redis源碼研究之RDB持久化

本文主要說明Redis的RDB持久化方式的實現(xiàn)及運作機制歹茶。

推薦閱讀:
1亏娜、Redis 的RDB持久化方式的理論說明見: Redis之RDB持久化小探

I腊瑟、核心數(shù)據(jù)結構rio

持久化的IO操作在rio.hrio.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ū)別在于是否阻塞客戶端服務觉增,因為bgsavefork子進程的方式完成的。這里重點說明以子進程的方式完成持久化:

通過調用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源碼日志》

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市权埠,隨后出現(xiàn)的幾起案子榨了,更是在濱河造成了極大的恐慌,老刑警劉巖攘蔽,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件龙屉,死亡現(xiàn)場離奇詭異,居然都是意外死亡满俗,警方通過查閱死者的電腦和手機转捕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來唆垃,“玉大人五芝,你說我怎么就攤上這事≡颍” “怎么了枢步?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蓄坏。 經常有香客問我价捧,道長,這世上最難降的妖魔是什么涡戳? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任结蟋,我火速辦了婚禮,結果婚禮上渔彰,老公的妹妹穿的比我還像新娘嵌屎。我一直安慰自己,他們只是感情好恍涂,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布宝惰。 她就那樣靜靜地躺著,像睡著了一般再沧。 火紅的嫁衣襯著肌膚如雪尼夺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天炒瘸,我揣著相機與錄音淤堵,去河邊找鬼。 笑死顷扩,一個胖子當著我的面吹牛拐邪,可吹牛的內容都是我干的。 我是一名探鬼主播隘截,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼扎阶,長吁一口氣:“原來是場噩夢啊……” “哼汹胃!你這毒婦竟也來了?” 一聲冷哼從身側響起东臀,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤着饥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后啡邑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贱勃,經...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年谤逼,在試婚紗的時候發(fā)現(xiàn)自己被綠了贵扰。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡流部,死狀恐怖戚绕,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情枝冀,我是刑警寧澤舞丛,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站果漾,受9級特大地震影響球切,放射性物質發(fā)生泄漏。R本人自食惡果不足惜绒障,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一吨凑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧户辱,春花似錦鸵钝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至必逆,卻和暖如春怠堪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背名眉。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工粟矿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人璧针。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓嚷炉,卻偏偏與公主長得像渊啰,于是被迫代替她去往敵國和親探橱。 傳聞我的和親對象是個殘疾皇子申屹,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內容

  • 簡介 Redis 持久化 RDB、AOF 為防止數(shù)據(jù)丟失隧膏,需要將 Redis 中的數(shù)據(jù)從內存中 dump 到磁盤哗讥,...
    翼徳閱讀 413評論 0 0
  • 本文檔翻譯自http://redis.io/topics/persistence。 這篇文章提供了 Redis 持...
    daos閱讀 694評論 0 10
  • Redis持久化: 提供了多種不同級別的持久化方式:一種是RDB,另一種是AOF. RDB 持久化可以在指定的時間...
    不姓馬的小馬哥閱讀 641評論 0 10
  • Redis 持久化: 常用的兩種持久化 提供了多種不同級別的持久化方式:一種是RDB,另一種是AOF. RDB 持...
    邊學邊記閱讀 1,129評論 0 1
  • 公司與學校假期完美錯開胞枕,終于可以有個機會送女兒上學杆煞。 女兒一路上又說又笑手緊緊地牽著我,生怕走在半路與她分開腐泻。到校...
    么么小饞貓閱讀 169評論 0 0