鍍金池/ 教程/ 大數(shù)據(jù)/ RDB 持久化策略
Redis 數(shù)據(jù)淘汰機(jī)制
積分排行榜
小剖 Memcache
Redis 數(shù)據(jù)結(jié)構(gòu) intset
分布式鎖
從哪里開(kāi)始讀起,怎么讀
Redis 數(shù)據(jù)結(jié)構(gòu) dict
不在浮沙筑高臺(tái)
Redis 集群(上)
Redis 監(jiān)視器
源碼閱讀工具
Redis 日志和斷言
內(nèi)存數(shù)據(jù)管理
Redis 數(shù)據(jù)結(jié)構(gòu)綜述
源碼日志
Web 服務(wù)器存儲(chǔ) session
消息中間件
Redis 與 Lua 腳本
什么樣的源代碼適合閱讀
Redis 數(shù)據(jù)結(jié)構(gòu) sds
Memcached slab 分配策略
訂閱發(fā)布機(jī)制
Redis 是如何提供服務(wù)的
Redis 事務(wù)機(jī)制
Redis 集群(下)
主從復(fù)制
Redis 應(yīng)用
RDB 持久化策略
Redis 數(shù)據(jù)遷移
Redis 事件驅(qū)動(dòng)詳解
初探 Redis
Redis 與 Memcache
AOF 持久化策略
Redis 數(shù)據(jù)結(jié)構(gòu) redisOb
作者簡(jiǎn)介
Redis 數(shù)據(jù)結(jié)構(gòu) ziplist
Redis 數(shù)據(jù)結(jié)構(gòu) skiplist
Redis 哨兵機(jī)制

RDB 持久化策略

簡(jiǎn)介 Redis 持久化 RDB、AOF

為防止數(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 的操作。

數(shù)據(jù)結(jié)構(gòu) rio

持久化的 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)存中就不是持久化了。

RDB 持久化的運(yùn)作機(jī)制

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)景舉例:

  1. Redis 服務(wù)器初始化過(guò)程中,設(shè)定了定時(shí)事件,每隔一段時(shí)間就會(huì)觸發(fā)持久化操作; 進(jìn)入定時(shí)事件處理程序中,就會(huì) fork 產(chǎn)生子進(jìn)程執(zhí)行持久化操作。
  2. Redis 服務(wù)器預(yù)設(shè)了 save 指令,客戶端可要求服務(wù)器進(jìn)程中斷服務(wù),執(zhí)行持久化操作。

這里主要展開(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ù)的組織方式

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ì)研讀一下。