閱讀49 返回首頁    go 阿裏雲 go 技術社區[雲棲]


redis4.0之RDB-AOF混合持久化

前言

redis有兩種持久化的方式——RDB和AOFRDB是一份內存快照AOF則為可回放的命令日誌他們兩個各有特點也相互獨立。4.0開始允許使用RDB-AOF混合持久化的方式結合了兩者的優點通過aof-use-rdb-preamble配置項可以打開混合開關。

RDB V.S. AOF

1. RDB

RDB文件本質上是一份內存快照保存了創建RDB文件那個時間點的redis全量數據具有數據文件小創建、恢複快的優點但是由於快照的特性無法保存創建RDB之後的增量數據。

2. AOF

AOF文件本質上是一份執行日誌保存所有對redis進行更改的命令增量數據也就隨命令寫入AOF文件刷盤的策略由配置項appendfsync控製可以選擇"everysec"或"always"。

AOF文件基本上是human-readable的文本所以其體積相對較大在從AOF文件恢複數據時就是做日誌回放執行AOF文件中記錄的所有命令所以相對RDB而言恢複耗時較長。

隨著redis的運行AOF文件會不斷膨脹由aofrewrite機製來防止磁盤空間被撐滿詳見上一篇文章《redis4.0之利用管道優化aofrewrite》

RDB-AOF混合持久化

細細想來aofrewrite時也是先寫一份全量數據到新AOF文件中再追加增量隻不過全量數據是以redis命令的格式寫入。那麼是否可以先以RDB格式寫入全量數據再追加增量日誌呢這樣既可以提高aofrewrite和恢複速度也可以減少文件大小還可以保證數據的完畢性整合RDB和AOF的優點那麼現在4.0實現了這一特性——RDB-AOF混合持久化。

aofrewrite

綜上所述RDB-AOF混合持久化體現在aofrewrite時那麼我們就從這裏開始來看4.0是如何實現的

  • 回憶下aofrewrite的過程

    無論是serverCron觸發或者執行BGREWRITEAOF命令最終redis都會走到rewriteAppendOnlyFileBackground()

    rewriteAppendOnlyFileBackground函數會fork子進程子進程進入rewriteAppendOnlyFile函數來生成新的AOF文件混合持久化就從這裏開始

int rewriteAppendOnlyFile(char *filename) {
    ...
    if (server.aof_use_rdb_preamble) {
        int error;
        if (rdbSaveRio(&aof,&error,RDB_SAVE_AOF_PREAMBLE,NULL) == C_ERR) {
            errno = error;
            goto werr;
        }
    } else {
        if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
    }
    ...
}
  • 可以看到當混合持久化開關打開時就會進入rdbSaveRio函數先以RDB格式來保存全量數據

    前文說道子進程在做aofrewrite時會通過管道從父進程讀取增量數據並緩存下來

    那麼在以RDB格式保存全量數據時也會從管道讀取數據並不會造成管道阻塞

int rdbSaveRio(rio *rdb, int *error, int flags, rdbSaveInfo *rsi) {
    ...
    snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION);
    if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;
    if (rdbSaveInfoAuxFields(rdb,flags,rsi) == -1) goto werr;
  • 首先把RDB的版本注意不是redis的版本和輔助域寫入文件
    for (j = 0; j < server.dbnum; j++) {
        redisDb *db = server.db+j;
        dict *d = db->dict;
        if (dictSize(d) == 0) continue;
        di = dictGetSafeIterator(d);
        if (!di) return C_ERR;

        /* Write the SELECT DB opcode */
        if (rdbSaveType(rdb,RDB_OPCODE_SELECTDB) == -1) goto werr;
        if (rdbSaveLen(rdb,j) == -1) goto werr;

        /* Write the RESIZE DB opcode. We trim the size to UINT32_MAX, which
         * is currently the largest type we are able to represent in RDB sizes.
         * However this does not limit the actual size of the DB to load since
         * these sizes are just hints to resize the hash tables. */
        uint32_t db_size, expires_size;
        db_size = (dictSize(db->dict) <= UINT32_MAX) ?
                                dictSize(db->dict) :
                                UINT32_MAX;
        expires_size = (dictSize(db->expires) <= UINT32_MAX) ?
                                dictSize(db->expires) :
                                UINT32_MAX;
        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;
  • 然後遍曆DB先把dbnum和db_size、expires_size寫入文件
        /* Iterate this DB writing every entry */
        while((de = dictNext(di)) != NULL) {
            sds keystr = dictGetKey(de);
            robj key, *o = dictGetVal(de);
            long long expire;

            initStaticStringObject(key,keystr);
            expire = getExpire(db,&key);
            if (rdbSaveKeyValuePair(rdb,&key,o,expire,now) == -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 (flags & RDB_SAVE_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. */
  • 在當前DB中遍曆所有的key把key-value對及過期時間如果有設置的話寫入文件

    這裏小插曲一下在rdbSaveKeyValuePair函數中會判斷expire是否已經到了過期時間如果已經過期就不會寫入文件

    同時如果flags標記了RDB_SAVE_AOF_PREAMBLE的話說明是在aofrewrite且開啟了RDB-AOF混合開關此時就會從父進程去讀取增量數據了

    /* EOF opcode */
    if (rdbSaveType(rdb,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. */
    cksum = rdb->cksum;
    memrev64ifbe(&cksum);
    if (rioWrite(rdb,&cksum,8) == 0) goto werr;
    return C_OK;
}
  • 最後把代表RDB格式結束的RDB_OPCODE_EOF標記和校驗和寫入文件

RDB-AOF混合持久化的RDB部分到此結束rdbSaveRio函數運行完後返回rewriteAppendOnlyFile繼續把增量數據寫入AOF文件。

也就是說AOF文件的前半段是RDB格式的全量數據後半段是redis命令格式的增量數據。

數據恢複

當appendonly配置項為no時redis啟動後會去加載RDB文件以RDB格式來解析RDB文件自然沒有問題。

而appendonly配置項為yes時redis啟動後會加載AOF文件來恢複數據如果持久化時開啟了RDB-AOF混合開關那麼AOF文件的前半段就是RDB格式此時要如何正確加載數據呢

一切數據都逃不過協議二字不以正確的協議存儲和解析那就是亂碼既然允許RDB-AOF混合持久化就要能夠識別並恢複數據這一節我們來介紹如何以正確的姿勢來恢複數據。

加載AOF文件的入口為loadAppendOnlyFile

int loadAppendOnlyFile(char *filename) {
    ...
    /* Check if this AOF file has an RDB preamble. In that case we need to
     * load the RDB file and later continue loading the AOF tail. */
    char sig[5]; /* "REDIS" */
    if (fread(sig,1,5,fp) != 5 || memcmp(sig,"REDIS",5) != 0) {
        /* No RDB preamble, seek back at 0 offset. */
        if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
    } else {
        /* RDB preamble. Pass loading the RDB functions. */
        rio rdb;

        serverLog(LL_NOTICE,"Reading RDB preamble from AOF file...");
        if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
        rioInitWithFile(&rdb,fp);
        if (rdbLoadRio(&rdb,NULL) != C_OK) {
            serverLog(LL_WARNING,"Error reading the RDB preamble of the AOF file, AOF loading aborted");
            goto readerr;
        } else {
            serverLog(LL_NOTICE,"Reading the remaining AOF tail...");
        }
    }
    ...
}

打開AOF文件之後首先讀取5個字符如果是"REDIS"那麼就說明這是一個混合持久化的AOF文件正確的RDB格式一定是以"REDIS"開頭而純AOF格式則一定以"*"開頭此時就會進入rdbLoadRio函數來加載數據。

rdbLoadRio函數此處就不詳細展開了就是以約定好的協議解析文件內容直至遇到RDB_OPCODE_EOF結束標記返回loadAppendOnlyFile函數繼續以AOF格式解析文件直到結束整個加載過程完成。

附錄

1. RDB格式的文件

我們先向redis寫入一些數據並生成RDB文件

$redis-cli
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> expire foo 60
(integer) 1
127.0.0.1:6379> select 2
OK
127.0.0.1:6379[2]> set foo bar
OK
127.0.0.1:6379[2]> bgsave
Background saving started

看下RDB文件內容

$cat dump.rdb
REDIS0008    redis-ver4.0.1
redis-bits@ctimeYused-mem 
                                aof-preamblerepl-id(484f9d49a700c4b9b136f0fd40d2d6e5a8460438
                                                                                               repl-offa;^foobarfoobar^KJ_U

OMG...一堆亂碼隱約可以看到一些和redis相關的字符串為了更直觀的感受下RDB的內容我們用redis自帶的工具redis-check-rdb來看下

redis-check-rdb dump.rdb
[offset 0] Checking RDB file dump.rdb
[offset 26] AUX FIELD redis-ver = '4.0.1'
[offset 40] AUX FIELD redis-bits = '64'
[offset 52] AUX FIELD ctime = '1504234774'
[offset 67] AUX FIELD used-mem = '2139016'
[offset 83] AUX FIELD aof-preamble = '0'
[offset 133] AUX FIELD repl-id = '484f9d49a700c4b9b136f0fd40d2d6e5a8460438'
[offset 148] AUX FIELD repl-offset = '0'
[offset 150] Selecting DB ID 0
[offset 173] Selecting DB ID 2
[offset 194] Checksum OK
[offset 194] \o/ RDB looks OK! \o/
[info] 2 keys read
[info] 1 expires
[info] 0 already expired

這下就好看多了首先可以看到是一些AUX FIELD輔助域4.0特有用來配合全新的主從同步方式PSYNC2後麵會專門來介紹PSYNC2然後可以看到DB0和DB2是有內容的Checksum也OK最後是說一共有2個key其中一個設置了過期時間到目前為止還都沒有過期。

2. AOF格式的文件

同樣的數據我們來看下在AOF文件中是如何保存的

$cat appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
set
$3
foo
$3
bar
*3
$9
PEXPIREAT
$3
foo
$13
1504255377087
*2
$6
SELECT
$1
2
*3
$3
set
$3
foo
$3
bar

很明顯就是一條一條redis命令。

3. RDB-AOF混和持久化的文件

最後來看下RDB-AOF混和持久化的文件。

首先打開混合開關執行BGREWRITEAOF生成RDB-AOF混合文件再追加寫入一些數據

$redis-cli
127.0.0.1:6379> config set aof-use-rdb-preamble yes
OK
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> quit

再來看下此時AOF文件內容

$cat appendonly.aof
REDIS0008    redis-ver4.0.1
redis-bits@ctimeYused-memP 
                                  aof-preamblerepl-id(484f9d49a700c4b9b136f0fd40d2d6e5a8460438
                                                                                                 repl-offsetfoobar?I    Y*2
$6
SELECT
$1
0
*3
$3
set
$3
foo
$3
bar

顯而易見前半段是RDB格式的全量數據後半段是redis命令格式的增量數據。

基於4.0的雲redis正在籌備之中敬請期待。

最後更新:2017-09-04 10:32:27

  上一篇:go  MyRocks之bloom filter
  下一篇:go  BLOCKED,WAITING,TIMED_WAITING有什麼區別?-用生活的例子解釋