Redis源碼剖析之AOF

書接上回,上回咱們詳細講解了Redis的RDB機制,RDB解決了redis數據持久化一部分的問題,爲何說一部分?由於rdb是redis中某一時刻的快照,那麼在此次快照後若是數據有新的變動,它是不會被持久化下來的,必須得等到下次rdb備份。然而,生成rdb是和消耗性能的,因此它就不適合很頻繁生成。Redis爲了彌補這一不足提供了AOF。 html

AOF的全稱是AppendOnlyFile,源碼在aof.c。其實關鍵就是Append(追加),核心原理很簡單,就是若是執行完命令(set,del,expire……)後,發現有數據變更,就將此次操做做爲一條日誌記錄到aof文件裏,若是有宕機就從新加載aof文件,重放全部的改動命令就能夠恢復數據了。只要日誌被完整刷到了磁盤上,數據就不會丟失。linux

配置

AOF的配置比較簡單,只有以下幾項。git

appendonly no  # aof開關,默認關閉
appendfilename "appendonly.aof"  # 保存的文件名,默認appendonly.aof
# 有三種刷數據的策略
appendfsync always  # always是隻要有數據改動,就把數據刷到磁盤裏,最安全但性能也最差
appendfsync everysec  # 每隔一秒鐘刷一次數據,數據安全性和性能折中,這也是redis默認和推薦的配置。 
appendfsync no # 不主動刷,何時數據刷到磁盤裏取決於操做系統,在大多數Linux系統中每30秒提交一次,性能最好,但數據安全性最差。

源碼

AOF的觸發

aof如何實現,又是怎麼被觸發的,讓咱們詳細看下源碼。
server.c中的void call(client *c, int flags) 是redis接受到client請求後處理請求的入口,其中會檢測Redis中的數據有沒有發生變化。若是有變化就會執行propagate()函數。github

dirty = server.dirty;
    prev_err_count = server.stat_total_error_replies;
    updateCachedTime(0);
    elapsedStart(&call_timer);
    c->cmd->proc(c); // 執行命令
    const long duration = elapsedUs(call_timer);
    c->duration = duration;
    dirty = server.dirty-dirty;
    if (dirty < 0) dirty = 0;
void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
               int flags)
{
    if (server.in_exec && !server.propagate_in_transaction)
        execCommandPropagateMulti(dbid);

    /* This needs to be unreachable since the dataset should be fixed during 
     * client pause, otherwise data may be lossed during a failover. */
    serverAssert(!(areClientsPaused() && !server.client_pause_in_transaction));

    if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)
        feedAppendOnlyFile(cmd,dbid,argv,argc); // 若是aof開啓了,就會向aof傳播該命令。
    if (flags & PROPAGATE_REPL)
        replicationFeedSlaves(server.slaves,dbid,argv,argc);
}

propagate函數的做用就是將帶來數據改動的命令傳播給slave和AOF,這裏咱們只關注AOF,咱們來詳細看下feedAppendOnlyFile()函數。redis

AOF數據生成

void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    sds buf = sdsempty();
    /* The DB this command was targeting is not the same as the last command
     * we appended. To issue a SELECT command is needed. */
    if (dictid != server.aof_selected_db) {
        char seldb[64];

        snprintf(seldb,sizeof(seldb),"%d",dictid);
        buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
            (unsigned long)strlen(seldb),seldb);
        server.aof_selected_db = dictid;
    }

    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == expireatCommand) {
        /* 把 EXPIRE/PEXPIRE/EXPIREAT 命令轉化爲 PEXPIREAT 命令*/
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
    } else if (cmd->proc == setCommand && argc > 3) {
        robj *pxarg = NULL;
        /* When SET is used with EX/PX argument setGenericCommand propagates them with PX millisecond argument.
         * So since the command arguments are re-written there, we can rely here on the index of PX being 3. */
        if (!strcasecmp(argv[3]->ptr, "px")) {
            pxarg = argv[4];
        }
        /* 把set命令的expired所帶的相對時間轉化爲絕對時間(ms). */
        if (pxarg) {
            robj *millisecond = getDecodedObject(pxarg);
            long long when = strtoll(millisecond->ptr,NULL,10);
            when += mstime();

            decrRefCount(millisecond);

            robj *newargs[5];
            newargs[0] = argv[0];
            newargs[1] = argv[1];
            newargs[2] = argv[2];
            newargs[3] = shared.pxat;
            newargs[4] = createStringObjectFromLongLong(when);
            buf = catAppendOnlyGenericCommand(buf,5,newargs);
            decrRefCount(newargs[4]);
        } else {
            buf = catAppendOnlyGenericCommand(buf,argc,argv);
        }
    } else {
        /* 其餘的命令都不須要轉化 */
        buf = catAppendOnlyGenericCommand(buf,argc,argv);
    }

    /* 追加到AOF緩衝區。在從新進入事件循環以前,數據將被刷新到磁盤上,所以在客戶端在執行前就會獲得回覆。*/
    if (server.aof_state == AOF_ON)
        server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));

    /* 若是後臺正在進行AOF重寫,咱們但願將子數據庫和當前數據庫之間的差別累積到緩衝區中,
     * 以便在子進程執行其工做時,咱們能夠將這些差別追加到新的只追加文件中。 */
    if (server.child_type == CHILD_TYPE_AOF)
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));

    sdsfree(buf);
}

這裏沒有啥太複雜的邏輯,就是將命令轉化爲RESP協議格式的字符串(RESP協議後續會詳解),而後追加到server.aof_buf中,這時候AOF數據還都在緩衝區中,並無寫入到磁盤中,那buf中的數據什麼時候寫入磁盤呢?數據庫

刷數據

刷數據的核心代碼在flushAppendOnlyFile()中,flushAppendOnlyFile在serverCron、beforeSleep和prepareForShutdown中都有被調用,它的做用就是將緩衝區的數據寫到磁盤中,代碼比較長且複雜,但大部分都是異常處理和性能監控,忽略掉這部分後代碼也比較容易理解,這裏就再也不羅列了,詳見aof.c安全

RDB vs AOF

最後,咱們來對比下RDB和AOF,他們各自都有啥優缺點,該如何選用。服務器

RDB的優點

  1. RDB是壓縮的後緊湊數據格式,比較很適合備份,
  2. 一樣的數據量下,rdb的文件大小會很小,比較適合傳輸和數據恢復。
  3. RDB對Redis的讀寫性能影響小,生成RDB的時redis主進程會fork出一個子進程,不會影響到主進程的讀寫。
  4. RDB數據加載更快,恢復起來更快。

RDB的缺點

  1. RDB是按期備份,若是備份前發生宕機,數據可能會丟失。
  2. RDB的生成依賴於linux的fork,若是數據量比較大的話,很影響服務器性能。

AOF的優點

  1. AOF是持續性備份,能夠儘量保證數據不丟失。
  2. Redis太大時,Redis能夠在後臺自動重寫AOF。重寫是徹底安全的,由於Redis繼續追加到舊文件時,會生成一個全新的文件,其中包含建立當前數據集所需的最少操做集,一旦準備好第二個文件,Redis會切換這兩個文件並開始追加到新的那一個。
  3. AOF文件格式簡單,易於解析。app

    AOF的缺點

  4. 對於同一數據集,AOF文件大小一般大於等效的RDB文件。
  5. 若是使用fsync策略,AOF可能比RDB慢。

RDB和AOF該如何選

若是是要求極致的性能,但對數據恢復不敏感,兩者能夠都不要,若是是關注性能且關注數據可用性,但不要求數據完整性,能夠選用RDB。若是說很是關注數據完整性和宕機恢復的能力,能夠RDB+AOF同時開啓。函數

參考資料

  1. Redis persistence demystified
  2. Redis Persistence
本文是Redis源碼剖析系列博文,同時也有與之對應的Redis中文註釋版,有想深刻學習Redis的同窗,歡迎star和關注。
Redis中文註解版倉庫: https://github.com/xindoo/Redis
Redis源碼剖析專欄: https://zxs.io/s/1h
若是以爲本文對你有用,歡迎 一鍵三連
相關文章
相關標籤/搜索