Redis的AOF持久化深入解析

Redis提供兩種持久化方式,RDB和AOF;與RDB不同,AOF可以完整的記錄整個數據庫,而不像RDB只是數據庫某一時刻的快照; 

那麼AOF模式爲什麼可以完整的記錄整個數據庫呢? 

原理:在AOF模式下,Redis會把執行過的每一條更新命令記錄下來,保存到AOF文件中;當Redis需要恢復數據庫數據時,只需要從之前保存的AOF文件中依次讀取命令,執行即可 eg. 

Shell代碼  收藏代碼
  1. 我們執行了以下命令:  
  2. redis 127.0.0.1:6379> set name diaocow  
  3. OK  
  4. redis 127.0.0.1:6379> lpush country china usa  
  5. (integer) 4  
  6.   
  7. 這時候在AOF文件中的類容類似下面:  
  8. *3\r\n$3\r\nset\r\n$4\r\nname\r\n$7\r\ndiaocow\r\n  
  9. *4\r\n$5\r\nlpush\r\n$7\r\ncountry\r\n$5\r\nchina\r\n$3\r\nusa\r\n  

看了上面的內容,我想不用我過多解釋,你也能大致猜出AOF協議格式,因爲它實在太簡單明瞭了

協議格式爲:

*<count>\r\n<element1>…<elementN>

每個參數element格式:$<length>\r\n<content>\r\n

其中,不包含<>字符,count表示參數個數,length表示參數的字節長度,content表示參數的內容

(1)對於一般的寫入命令(SET、SADD、ZADD、RPUSH等)+PEXPIREAT命令

sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
    char buf[32];
    int len, j;
    robj *o;

    // 重建命令的個數,格式爲 *<count>\r\n
    // 例如 *3\r\n
    buf[0] = '*';
    len = 1+ll2string(buf+1,sizeof(buf)-1,argc);
    buf[len++] = '\r';
    buf[len++] = '\n';
    dst = sdscatlen(dst,buf,len);

    // 重建命令和命令參數,格式爲 $<length>\r\n<content>\r\n
    // 例如 $3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
    for (j = 0; j < argc; j++) {
        o = getDecodedObject(argv[j]);

        // 組合 $<length>\r\n
        buf[0] = '$';
        len = 1+ll2string(buf+1,sizeof(buf)-1,sdslen(o->ptr));
        buf[len++] = '\r';
        buf[len++] = '\n';
        dst = sdscatlen(dst,buf,len);

        // 組合 <content>\r\n
        dst = sdscatlen(dst,o->ptr,sdslen(o->ptr));
        dst = sdscatlen(dst,"\r\n",2);

        decrRefCount(o);
    }

    // 返回重建後的協議內容
    return dst;
}

(2)設置鍵的過期時間命令:EXPIRE 、 PEXPIRE 和 EXPIREAT。

處理這三種命令,最終會轉換爲PEXPIREAT來執行。

sds catAppendOnlyExpireAtCommand(sds buf, struct redisCommand *cmd, robj *key, robj *seconds) {
    long long when;
    robj *argv[3];

    /* Make sure we can use strtol 
     *
     * 取出過期值
     */
    seconds = getDecodedObject(seconds);
    when = strtoll(seconds->ptr,NULL,10);

    /* Convert argument into milliseconds for EXPIRE, SETEX, EXPIREAT 
     *
     * 如果過期值的格式爲秒,那麼將它轉換爲毫秒
     */
    if (cmd->proc == expireCommand || cmd->proc == setexCommand ||
        cmd->proc == expireatCommand)
    {
        when *= 1000;
    }

    /* Convert into absolute time for EXPIRE, PEXPIRE, SETEX, PSETEX 
     *
     * 如果過期值的格式爲相對值,那麼將它轉換爲絕對值
     */
    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == setexCommand || cmd->proc == psetexCommand)
    {
        when += mstime();
    }

    decrRefCount(seconds);

    // 構建 PEXPIREAT 命令
    argv[0] = createStringObject("PEXPIREAT",9);
    argv[1] = key;
    argv[2] = createStringObjectFromLongLong(when);

    // 追加到 AOF 緩存中
    buf = catAppendOnlyGenericCommand(buf, 3, argv);

    decrRefCount(argv[0]);
    decrRefCount(argv[2]);

    return buf;
}

可以看出上述函數中經過處理後,調用的依然是catAppendOnlyGenericCommand函數。

(3)對於SETEX和PSETEX命令

因爲常見格式爲:SETEXkey ttl value

所以對於命令參數來說,arg[1]爲key,arg[2]爲過期時間,argv[3]爲value

在後面的feedAppendOnlyFile函數中會看到:

對於這兩種命令,先翻譯爲SETkey value命令,接着翻譯爲PEXPIREAT key ttl_ms命令,依次執行上述兩個邏輯函數:

// EXPIRE 、 PEXPIRE 和 EXPIREAT 命令
    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == expireatCommand) {
        /* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT 
         *
         * 將 EXPIRE 、 PEXPIRE 和 EXPIREAT 都翻譯成 PEXPIREAT
         */
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);

    // SETEX 和 PSETEX 命令
    } else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
        /* Translate SETEX/PSETEX to SET and PEXPIREAT 
         *
         * 將兩個命令都翻譯成 SET 和 PEXPIREAT
         */

        // SET
        tmpargv[0] = createStringObject("SET",3);
        tmpargv[1] = argv[1];
        tmpargv[2] = argv[3];
        buf = catAppendOnlyGenericCommand(buf,3,tmpargv);

        // PEXPIREAT
        decrRefCount(tmpargv[0]);
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);

    // 其他命令
    } else {
        /* All the other commands don't need translation or need the
         * same translation already operated in the command vector
         * for the replication itself. */
        buf = catAppendOnlyGenericCommand(buf,argc,argv);
    }
Redis把更新命令記錄到AOF文件,分爲兩個階段: 

階段1:把更新命令寫入aof緩存 




在每次調用執行更新命令後,根據設置選項(開啓AOF、重寫子進程)來判斷是否執行追加到AOF緩存區(aof_buf)和重寫緩

存區(aof_rewrite_buf_blocks)。

這裏提一下,AOF緩衝區aof_buf爲sds格式,重寫緩存區(aof_rewrite_buf_blocks)則是一個list鏈表集合,具體類型定義爲

aofrwblock,這樣做的原因是考慮到分配到一個非常大的空間並不總是可能的,也可能產生大量的複製工作,所以這裏採用多個

大小爲AOF_RW_BUF_BLOCK_SIZE(10M)的空間來保存命令協議格式。aofrwblock定義爲:

typedef struct aofrwblock {
    
    // 緩存塊已使用字節數和可用字節數
    unsigned long used, free;

    // 緩存塊
    char buf[AOF_RW_BUF_BLOCK_SIZE];

} aofrwblock;

設計到該類型的API有:

void aofRewriteBufferReset(void):釋放舊的鏈表,初始化新的鏈表,用於AOF重寫緩存的初始化
unsigned long aofRewriteBufferSize(void):返回AOF重寫緩存當前已使用的大小
void aofRewriteBufferAppend(unsigned char *s, unsigned long len):將字符數組s追加到AOF重寫緩存的末尾,不夠的話繼續分配新的緩存塊
ssize_t aofRewriteBufferWrite(int fd):將重寫緩存中的所有內容(可能有多個塊組成)寫入到給定fd中,返回寫入的字節數量,錯誤返回-1

具體的緩存區追加函數爲feedAppendOnlyFile,下面給出簡約的僞代碼。

Python代碼  

  1. def processCommand(cmd, argc, argv):  
  2.     # 執行命令  
  3.     call(cmd, argc, argv)  
  4.     # 該命令變更了鍵空間並且AOF模式打開  
  5.     if redisServer.update_key_space and redisServer.aof_state & REDIS_AOF_ON:  
  6.         feedAppendOnlyFile(cmd, argc, argv)   
  7.   
  8. def feedAppendOnlyFile(cmd, argc, argv):  
  9.     # 把命令轉換成AOF協議格式  
  10.     aofCmdStr = getAofProtocolStr(cmd, argc, argv)  
  11.     redisServer.aof_buf.append(aofCmdStr )  
  12.   
  13.     # 存在一個子進程正在進行AOF_REWRITE(關於AOF_REWRITE,稍後詳說)  
  14.     if redisServer.aof_child_pid != -1:  
  15.         redisServer.aof_rewrite_buf_blocks.append(aofCmdStr )  

階段2: 把aof緩存寫入文件

當我們開始下一次事件循環(eventLoop)之前,redis會把AOF緩存中的內容寫入到文件: 
Python代碼  收藏代碼
  1. def flushAppendOnlyFile(force):    
  2.     if len(redisServer.aof_buf) == 0:  
  3.         return  
  4.     # 把緩存數據寫入文件    
  5.     if writeByPolicy(force, redisServer.aof_fsync):     
  6.         write(redisServer.aof_fd, redisServer.aof_buf, len(redisServer.aof_buf))     
  7.     # 同步數據到硬盤    
  8.     if fsyncByPolicy(force, redisServer.aof_fsync):    
  9.         fsync(redisServer.aof_fd)   

更多細節請看: aof.c/flushAppendOnlyFile函數 (ps: 這個函數代碼看起來比較晦澀) 

看到這裏,你也許會有兩個疑問: 
1. 爲什麼要調用fsync函數,不是已經調用write把數據寫入到文件了嗎? 
2. 僞代碼中aof_fsync是什麼,它有幾種類型? 

首先回答問題1,爲什麼寫入文件後,還要調用fsync函數: 
大多數unix系統爲了減少磁盤IO,採用了“延遲寫”技術,也就是說當我們執行完write調用後,數據並不一定立馬被寫入磁盤(可能還是保留在系統的buffer cache或者page cache中),這樣當主機突然斷電,這些我們本以爲已經寫入到磁盤文件的數據可能就會丟失;所以當我們需要確保數據被完整正確的寫入磁盤(譬如數據庫的持久化),則需要調用同步函數fsync,它會一直阻塞直到數據全部被寫入到硬盤 

問題2,aof_fysnc是什麼: 
aof_fsync用來指定flush策略,也就是調用fsync函數的策略,它一共有三種: 
a. AOF_FSYNC_NO :每次都會把aof_buf中的內容寫入到磁盤,但是不會調用fsync函數; 
b. AOF_FSYNC_ALWAYS :每次都會把aof_buf中的內容寫入到磁盤,同時調用fsync函數; (主進程負責)
c. AOF_FSYNC_EVERYSEC :每次都會把aof_buf中的內容寫入到磁盤,如果距離上次同步超過一秒則調用fsync函數,由子線程負責。(默認值)

由於AOF_FSYNC_ALWAYS每次都寫入文件都會調用fsync,所以這種flush策略可以保證數據的完整性,缺點就是性能太差(因爲fysnc是個同步調用,會阻塞主進程對客戶端請求的處理),而AOF_FSYNC_NO由於依賴於操作系統自動sync,因此不能保證數據的完整性; 

那有沒有一種折中的方式:既能不過分降低系統的性能,又能最大程度上的保證數據的完整性,答案就是:AOF_FSYNC_EVERYSEC,AOF_FSYNC_EVERYSEC的flush策略是:定期(至少1s)去調用fsync,並且該操作是放到一個異步隊列中(線程)去執行,因此不會阻塞主進程 


AOF 後臺執行的方式和 RDB 有類似的地方,fork 一個子進程,主進程仍進行服務,子進程執行 AOF 持久化,數據被 dump 到磁盤上。與 RDB 不同的是,後臺子進程持久化過程中,主進程會記錄期間的所有數據變更(主進程還在服務),並存儲在 server.aof_rewrite_buf_blocks 中;後臺子進程結束後,redis 更新緩存追加到 AOF 文件中,是 RDB 持久化所不具備的.


AOF模式至此我們已經基本說完,但是隨着Redis運行,AOF文件會變得越來越大(在業務高分期增長的更快),原因有兩個: 
a. AOF協議本身是文本協議,比較佔空間; 
b. Redis需要記錄從開始到現在的所有更新命令; 

這兩個原因導致了AOF文件容易變得很大,那有什麼方式可以優化嗎?譬如用戶執行了三個命令:lpush name diaocow; lpush name jack; lpush name jobs 
AOF文件會記錄以下數據: 
Aof_file代碼  收藏代碼
  1. *3\r\n$5\r\nlpush\r\n$4\r\nname\r\n$7\r\ndiaocow  
  2. *3\r\n$5\r\nlpush\r\n$4\r\nname\r\n$4\r\njack  
  3. *3\r\n$5\r\nlpush\r\n$4\r\nname\r\n$4\r\njobs  

但其實只需要記錄一條:lpush name diaocow jack jbos 命令即可: 
Aof_file代碼  收藏代碼
  1. *5\r\n$5\r\nlpush\r\n$4\r\nname\r\n$7\r\ndiaocow\r\n$4\r\njack\r\n$4\r\njobs  

所以當AOF文件達到 REDIS_AOF_REWRITE_MIN_SIZE(1M)時,Redis就會執行AOF_REWRITE來優化AOF文件; 

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 

AOF_REWRITE觸發條件 
1. 被動: 當AOF文件尺寸超過REDIS_AOF_REWRITE_MIN_SIZE & 達到一定增長比; 
2. 主動: 調用BGREWRITEAOF命令; 

主動和被動方式的AOF_REWRITE過程基本相同,唯一的區別就是,通過BGREWRITEAOF命令執行的AOF_REWRITE(主動)是在一個子進程中進行,因此它不會阻塞主進程對客戶端請求的處理,而被動方式由於是在主進程中進行,所以在AOF_REWRITE過程中redis是無法響應客戶端請求的; 

下面我就以BGREWRITEAOF命令爲例,具體看下AOF_REWRITE過程:


上述執行命令過程的代碼爲:

void bgrewriteaofCommand(redisClient *c) {

    // 不能重複運行 BGREWRITEAOF
    if (server.aof_child_pid != -1) {
        addReplyError(c,"Background append only file rewriting already in progress");

    // 如果正在執行 BGSAVE ,那麼預定 BGREWRITEAOF
    // 等 BGSAVE 完成之後, BGREWRITEAOF 就會開始執行
    } else if (server.rdb_child_pid != -1) {
        server.aof_rewrite_scheduled = 1;
        addReplyStatus(c,"Background append only file rewriting scheduled");

    // 執行 BGREWRITEAOF
    } else if (rewriteAppendOnlyFileBackground() == REDIS_OK) {
        addReplyStatus(c,"Background append only file rewriting started");

    } else {
        addReply(c,shared.err);
    }
}

執行rewriteAppendOnlyFileBackground的代碼爲:

/* This is how rewriting of the append only file in background works:
 * 
 * 以下是後臺重寫 AOF 文件(BGREWRITEAOF)的工作步驟:
 *
 * 1) The user calls BGREWRITEAOF
 *    用戶調用 BGREWRITEAOF
 *
 * 2) Redis calls this function, that forks():
 *    Redis 調用這個函數,它執行 fork() :
 *
 *    2a) the child rewrite the append only file in a temp file.
 *        子進程在臨時文件中對 AOF 文件進行重寫
 *
 *    2b) the parent accumulates differences in server.aof_rewrite_buf.
 *        父進程將新輸入的寫命令追加到 server.aof_rewrite_buf 中
 *
 * 3) When the child finished '2a' exists.
 *    當步驟 2a 執行完之後,子進程結束
 *
 * 4) The parent will trap the exit code, if it's OK, will append the
 *    data accumulated into server.aof_rewrite_buf into the temp file, and
 *    finally will rename(2) the temp file in the actual file name.
 *    The the new file is reopened as the new append only file. Profit!
 *
 *    父進程會捕捉子進程的退出信號,
 *    如果子進程的退出狀態是 OK 的話,
 *    那麼父進程將新輸入命令的緩存追加到臨時文件,
 *    然後使用 rename(2) 對臨時文件改名,用它代替舊的 AOF 文件,
 *    至此,後臺 AOF 重寫完成。
 */
int rewriteAppendOnlyFileBackground(void) {
    pid_t childpid;
    long long start;

    // 已經有進程在進行 AOF 重寫了
    if (server.aof_child_pid != -1) return REDIS_ERR;

    // 記錄 fork 開始前的時間,計算 fork 耗時用
    start = ustime();

	//fork函數執行時,創建一個子進程,複製一份副本給子進程,若fork成功執行,子進程返回0,父進程返回子進程的ID,失敗則返回-1
	//fork只調用一次,有兩個返回值,父子進程將同時擁有以下代碼,執行。
	//子進程
    if ((childpid = fork()) == 0) {
        char tmpfile[256];

        /* Child */

        // 關閉網絡連接 fd
        closeListeningSockets(0);

        // 爲進程設置名字,方便記認
        redisSetProcTitle("redis-aof-rewrite");

        // 創建臨時文件,臨時文件中的名字是由獲得進程的ID來設置,防止臨時文件重名,並進行 AOF 重寫
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) {
            size_t private_dirty = zmalloc_get_private_dirty();

            if (private_dirty) {
                redisLog(REDIS_NOTICE,
                    "AOF rewrite: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }
            // 發送重寫成功信號
            exitFromChild(0);
        } else {
            // 發送重寫失敗信號
            exitFromChild(1);
        }
    } else {
        /* Parent父進程 */
        // 記錄執行 fork 所消耗的時間
        server.stat_fork_time = ustime()-start;

        if (childpid == -1) {
            redisLog(REDIS_WARNING,
                "Can't rewrite append only file in background: fork: %s",
                strerror(errno));
            return REDIS_ERR;
        }

        redisLog(REDIS_NOTICE,
            "Background append only file rewriting started by pid %d",childpid);

        // 記錄 AOF 重寫的信息
        server.aof_rewrite_scheduled = 0;
        server.aof_rewrite_time_start = time(NULL);
        server.aof_child_pid = childpid;

        // 關閉字典自動 rehash
        updateDictResizePolicy();

        /* We set appendseldb to -1 in order to force the next call to the
         * feedAppendOnlyFile() to issue a SELECT command, so the differences
         * accumulated by the parent into server.aof_rewrite_buf will start
         * with a SELECT statement and it will be safe to merge. 
         *
         * 將 aof_selected_db 設爲 -1 ,
         * 強制讓 feedAppendOnlyFile() 下次執行時引發一個 SELECT 命令,
         * 從而確保之後新添加的命令會設置到正確的數據庫中
         */
        server.aof_selected_db = -1;
        replicationScriptCacheFlush();
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}

整個AOF_WRITE過程,最重要的一個函數是: rewriteAppendOnlyFile,它主要做了下面事情: 

a. 創建一個臨時文件temp-rewriteaof-pid.aof; 
b. 循環所有數據庫,把每一個數據庫中的鍵值對(過期鍵不寫入),按照aof協議寫入到臨時文件; 
c. 重命名臨時文件; 

注意的是,爲了避免在執行命令時造成客戶端輸入緩衝區溢出,重寫程序在處理列表、哈希表、集合、有序集合這四種可能會帶有多個元素的鍵時,會先檢查鍵所包含的元素數量,當超過了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那麼重寫程序將使用多條命令來記錄鍵的值,不單單使用一個命令,每條命令所恢復的元素個數最多不能超過上述常量值。

int rewriteAppendOnlyFile(char *filename) {
    dictIterator *di = NULL;
    dictEntry *de;
    rio aof;
    FILE *fp;
    char tmpfile[256];
    int j;
    long long now = mstime();

    /* Note that we have to use a different temp name here compared to the
     * one used by rewriteAppendOnlyFileBackground() function. 
     *
     * 創建臨時文件
     *
     * 注意這裏創建的文件名和 rewriteAppendOnlyFileBackground() 創建的文件名稍有不同
	 * 後者爲snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
     */
    snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) {
        redisLog(REDIS_WARNING, "Opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): %s", strerror(errno));
        return REDIS_ERR;
    }

    // 初始化文件 io
    rioInitWithFile(&aof,fp);

    // 設置每寫入 REDIS_AOF_AUTOSYNC_BYTES 字節
    // 就執行一次 FSYNC 
    // 防止緩存中積累太多命令內容,造成 I/O 阻塞時間過長
    if (server.aof_rewrite_incremental_fsync)
        rioSetAutoSync(&aof,REDIS_AOF_AUTOSYNC_BYTES);

    // 遍歷所有數據庫
    for (j = 0; j < server.dbnum; j++) {

        char selectcmd[] = "*2\r\n$6\r\nSELECT\r\n";

        redisDb *db = server.db+j;

        // 指向鍵空間
        dict *d = db->dict;
        if (dictSize(d) == 0) continue;

        // 創建鍵空間迭代器
        di = dictGetSafeIterator(d);
        if (!di) {
            fclose(fp);
            return REDIS_ERR;
        }

        /* SELECT the new DB 
         *
         * 首先寫入 SELECT 命令,確保之後的數據會被插入到正確的數據庫上
         */
        if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
        if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;

        /* Iterate this DB writing every entry 
         *
         * 遍歷數據庫所有鍵,並通過命令將它們的當前狀態(值)記錄到新 AOF 文件中
         */
        while((de = dictNext(di)) != NULL) {
            sds keystr;
            robj key, *o;
            long long expiretime;

            // 取出鍵
            keystr = dictGetKey(de);

            // 取出值
            o = dictGetVal(de);
            initStaticStringObject(key,keystr);

            // 取出過期時間
            expiretime = getExpire(db,&key);

            /* If this key is already expired skip it 
             *
             * 如果鍵已經過期,那麼跳過它,不保存
             */
            if (expiretime != -1 && expiretime < now) continue;

            /* Save the key and associated value 
             *
             * 根據值的類型,選擇適當的命令來保存值
             */
            if (o->type == REDIS_STRING) {
                /* Emit a SET command */
                char cmd[]="*3\r\n$3\r\nSET\r\n";
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                /* Key and value */
                if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                if (rioWriteBulkObject(&aof,o) == 0) goto werr;
            } else if (o->type == REDIS_LIST) {
                if (rewriteListObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_SET) {
                if (rewriteSetObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_ZSET) {
                if (rewriteSortedSetObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_HASH) {
                if (rewriteHashObject(&aof,&key,o) == 0) goto werr;
            } else {
                redisPanic("Unknown object type");
            }

            /* Save the expire time 
             *
             * 保存鍵的過期時間
             */
            if (expiretime != -1) {
                char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";

                // 寫入 PEXPIREAT expiretime 命令
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                if (rioWriteBulkLongLong(&aof,expiretime) == 0) goto werr;
            }
        }

        // 釋放迭代器
        dictReleaseIterator(di);
    }

    /* Make sure data will not remain on the OS's output buffers */
    // 沖洗並關閉新 AOF 文件
    if (fflush(fp) == EOF) goto werr;
    if (aof_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. 
     *
     * 原子地改名,用重寫後的新 AOF 文件覆蓋舊 AOF 文件
     */
    if (rename(tmpfile,filename) == -1) {
        redisLog(REDIS_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        return REDIS_ERR;
    }

    redisLog(REDIS_NOTICE,"SYNC append only file rewrite performed");

    return REDIS_OK;

werr:
    fclose(fp);
    unlink(tmpfile);
    redisLog(REDIS_WARNING,"Write error writing append only file on disk: %s", strerror(errno));
    if (di) dictReleaseIterator(di);
    return REDIS_ERR;
}

將重建各種對象所需的命令協議格式寫入到rio中:(在下列具體函數中會嚴格判斷每條命令包含的元素數量)

int rioWriteBulkObject(rio *r, robj *obj):obj指向的整數對象或者字符串對象

int rewriteListObject(rio*r, robj *key, robj *o):列表對象

int rewriteSetObject(rio *r, robj *key, robj *o):集合對象

int rewriteSortedSetObject(rio *r, robj *key, robj *o):有序集合對象

int rewriteHashObject(rio *r, robj *key, robj *o):哈希對象
當AOF_REWRITE過程執行完畢,Redis會用新生成的文件去替換原來的AOF文件,至此我們可以說,現在AOF文件中的內容已經是最精簡的了

現在還存在一個問題:如果我們是通過主動方式去執行AOF_REWRITE,那麼在保存AOF文件期間,“鍵空間”是可能發生變化的(因爲主進程沒有被阻塞),若直接用新生成的文件去替換原來的AOF文件,就會造成數據的不一致性(丟失在AOF_REWRITE過程中更新的數據) 

那redis如何解決這個問題呢? 在文章開頭講AOF模式的時候,我列舉了下面一段僞代碼: 
Python代碼  收藏代碼
  1. def processCommand(cmd, argc, argv):  
  2.     # 執行命令  
  3.     call(cmd, argc, argv)  
  4.     # 該命令變更了鍵空間並且AOF模式打開  
  5.     if redisServer.update_key_space and redisServer.aof_state & REDIS_AOF_ON:  
  6.         feedAppendOnlyFile(cmd, argc, argv)   
  7.   
  8. def feedAppendOnlyFile(cmd, argc, argv):  
  9.     # 把命令轉換成AOF協議格式  
  10.     aofCmdStr = getAofProtocolStr(cmd, argc, argv)  
  11.     redisServer.aof_buf.append(aofCmdStr )  
  12.   
  13.     # 存在一個子進程正在進行AOF_REWRITE  
  14.     if redisServer.aof_child_pid != -1:  
  15.         # 把變更命寫寫到aof重寫緩存  
  16.         redisServer.aof_rewrite_buf_blocks.append(aofCmdStr )  

你會發現,如果redis檢測到有一個子進程正在進行AOF_REWRITE,那麼它會把這期間所有變更命令寫到AOF重寫緩存(aof_rewrite_buf_blocks),然後當子進程完成AOF_REWRITE後,它會向父進程發送信號,父進程接受到子進程發來的信號,會將AOF重寫緩存中的內容追加到新生成文件(該過程執行函數爲backgroundRewriteDoneHandler,該函數在serverCron中會被判斷捕捉。AOF重寫緩存的追加過程會阻塞父進程,直至完成),這樣我們就可以保證數據的一致性,避免剛纔說的問題發生。

AOF文件的載入和還原

Redis讀取AOF文件並還原數據庫狀態的步驟爲:

1、創建一個不帶網絡連接的僞客戶端(fakeClient):因爲命令來源於AOF文件,非網絡連接

2、循環讀取每條指令,僞客戶端執行,直至所有寫命令處理完畢。

可參考aof.c/int loadAppendOnlyFile(char *filename)函數,該函數執行邏輯爲:

打開AOF文件並檢查,暫時性關閉AOF,建立僞客戶端:

開始從文件中循環:

每一次讀取內容到緩存,解析得到參數個數、每個參數,從命令表中尋找該命令,調用僞客戶端,執行命令,最後清理命令和命令參數對象。

循環結束,關閉AOF文件,釋放僞客戶端,恢復AOF狀態。

RDB和AOF持久化區別:

參考博客:https://blog.csdn.net/jackpk/article/details/30073097

總結:

1. 瞭解AOF協議

2. 瞭解AOF模式作用及原理

3. 瞭解AOF重寫作用及原理

參考博客:https://blog.csdn.net/erica_1230/article/details/51305552


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