Redis源碼剖析和註釋(十八)--- Redis AOF持久化機制

Redis AOF持久化機制

1. AOF持久化介紹

Redis中支持RDBAOF這兩種持久化機制,目的都是避免因進程退出,造成的數據丟失問題。

  • RDB持久化:把當前進程數據生成時間點快照(point-in-time snapshot)保存到硬盤的過程,避免數據意外丟失。
  • AOF持久化:以獨立日誌的方式記錄每次寫命令,重啓時在重新執行AOF文件中的命令達到恢復數據的目的。

Redis RDB持久化機制源碼剖析和註釋

AOF的使用:在redis.conf配置文件中,將appendonly設置爲yes,默認的爲no

2. AOF持久化的實現

AOF持久化所有註釋:Redis AOF持久化機制源碼註釋

2.1 命令寫入磁盤

2.1.1 命令寫入緩衝區

  • 命令問什麼先寫入緩衝區

由於Redis是單線程響應命令,所以每次寫AOF文件都直接追加到硬盤中,那麼寫入的性能完全取決於硬盤的負載,所以Redis會將命令寫入到緩衝區中,然後執行文件同步操作,再將緩衝區內容同步到磁盤中,這樣就很好的保持了高性能。

那麼緩衝區定義如下,它是一個簡單動態字符串(sds),因此很好的和C語言的字符串想兼容。

struct redisServer {
    // AOF緩衝區,在進入事件loop之前寫入
    sds aof_buf;      /* AOF buffer, written before entering the event loop */
};
  • 命令的寫入格式

Redis命令寫入的內容直接就是文本協議格式,例如:

*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n*5\r\n$4\r\nSADD\r\n$3\r\nkey\r\n$2\r\nm3\r\n$2\r\nm2\r\n$2\r\nm1\r\n

根據協議內容,大致可以得出:這是第0號數據庫,執行了一個SADD key m1 m2 m3命令。這就是Redis採用文件協議格式的原因之一,文本協議具有很高的可讀性,可以直接進行修改。而且,文本協議還具有很好的兼容性,而且協議採用了\r\n換行符,所以每次寫入命令只需執行追加操作。

既然是追加操作,因此,源碼中的函數名字也是如此,catAppendOnlyGenericCommand()函數實現了追加命令到緩衝區中,從這個函數中,可以清楚的看到協議是如何生成的。

// 根據傳入的命令和命令參數,將他們還原成協議格式
sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
    char buf[32];
    int len, j;
    robj *o;

    // 格式:"*<argc>\r\n"
    buf[0] = '*';
    len = 1+ll2string(buf+1,sizeof(buf)-1,argc);
    buf[len++] = '\r';
    buf[len++] = '\n';
    // 拼接到dst的後面
    dst = sdscatlen(dst,buf,len);

    // 遍歷所有的參數,建立命令的格式:$<command_len>\r\n<command>\r\n
    for (j = 0; j < argc; j++) {
        o = getDecodedObject(argv[j]);  //解碼成字符串對象
        buf[0] = '$';
        len = 1+ll2string(buf+1,sizeof(buf)-1,sdslen(o->ptr));
        buf[len++] = '\r';
        buf[len++] = '\n';
        dst = sdscatlen(dst,buf,len);
        dst = sdscatlen(dst,o->ptr,sdslen(o->ptr));
        dst = sdscatlen(dst,"\r\n",2);
        decrRefCount(o);
    }
    return dst; //返回還原後的協議內容
}

這個函數只是追加一個普通的鍵,然而一個過期命令的鍵,需要全部轉換爲PEXPIREAT,因爲必須將相對時間設置爲絕對時間,否則還原數據庫時,就無法得知該鍵是否過期,Redis的catAppendOnlyExpireAtCommand()函數實現了這個功能。

// 用sds表示一個 PEXPIREAT 命令,seconds爲生存時間,cmd爲指定轉換的指令
// 這個函數用來轉換 EXPIRE and PEXPIRE 命令成 PEXPIREAT ,以便在AOF時,時間總是一個絕對值
sds catAppendOnlyExpireAtCommand(sds buf, struct redisCommand *cmd, robj *key, robj *seconds) {
    long long when;
    robj *argv[3];

    /* Make sure we can use strtoll */
    // 解碼成字符串對象,以便使用strtoll函數
    seconds = getDecodedObject(seconds);
    // 取出過期值,long long類型
    when = strtoll(seconds->ptr,NULL,10);
    /* Convert argument into milliseconds for EXPIRE, SETEX, EXPIREAT */
    // 將 EXPIRE, SETEX, EXPIREAT 參數的秒轉換成毫秒
    if (cmd->proc == expireCommand || cmd->proc == setexCommand ||
        cmd->proc == expireatCommand)
    {
        when *= 1000;
    }
    /* Convert into absolute time for EXPIRE, PEXPIRE, SETEX, PSETEX */
    // 將 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);
    // 將命令還原成協議格式,追加到buf
    buf = catAppendOnlyGenericCommand(buf, 3, argv);
    decrRefCount(argv[0]);
    decrRefCount(argv[2]);
    // 返回buf
    return buf;
}

那麼,這兩個函數都是實現的底層功能,因此他們都被feedAppendOnlyFile()函數最終調用。

這個函數,創建一個空的簡單動態字符串(sds),將當前所有追加命令操作都追加到這個sds中,最終將這個sds追加到server.aof_buf。。還有就是,這個函數在寫入鍵之前,需要顯式的寫入一個SELECT命令,以正確的將所有鍵還原到正確的數據庫中。

// 將命令追加到AOF文件中
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    sds buf = sdsempty();   //設置一個空sds
    robj *tmpargv[3];

    // 使用SELECT命令,顯式的設置當前數據庫
    if (dictid != server.aof_selected_db) {
        char seldb[64];

        snprintf(seldb,sizeof(seldb),"%d",dictid);
        // 構造SELECT命令的協議格式
        buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
            (unsigned long)strlen(seldb),seldb);
        // 執行AOF時,當前的數據庫ID
        server.aof_selected_db = dictid;
    }

    // 如果是 EXPIRE/PEXPIRE/EXPIREAT 三個命令,則要轉換成 PEXPIREAT 命令
    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == expireatCommand) {
        /* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT */
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);

    // 如果是 SETEX/PSETEX 命令,則轉換成 SET and PEXPIREAT
    } else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
        /* Translate SETEX/PSETEX to SET and PEXPIREAT */
        // SETEX key seconds value
        // 構建SET命令對象
        tmpargv[0] = createStringObject("SET",3);
        tmpargv[1] = argv[1];
        tmpargv[2] = argv[3];
        // 將SET命令按協議格式追加到buf中
        buf = catAppendOnlyGenericCommand(buf,3,tmpargv);
        decrRefCount(tmpargv[0]);
        // 將SETEX/PSETEX命令和鍵對象按協議格式追加到buf中
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);

    // 其他命令直接按協議格式轉換,然後追加到buf中
    } else {
        buf = catAppendOnlyGenericCommand(buf,argc,argv);
    }

    // 如果正在進行AOF,則將命令追加到AOF的緩存中,在重新進入事件循環之前,這些命令會被沖洗到磁盤上,並向client回覆
    if (server.aof_state == AOF_ON)
        server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));

    // 如果後臺正在進行重寫,那麼將命令追加到重寫緩存區中,以便我們記錄重寫的AOF文件於當前數據庫的差異
    if (server.aof_child_pid != -1)
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));

    sdsfree(buf);
}

2.1.2 緩衝區同步到文件

既然緩衝區提供了高性能的保障,那麼緩衝區中的數據安全問題如何解決呢?只要數據存在於緩衝區,那麼就有丟失的危險。那麼,如果控制同步的頻率呢?Redis中給出了3中緩衝區同步文件的策略。

可配置值 說明
AOF_FSYNC_ALWAYS 命令寫入aof_buf後調用系統fsync和操作同步到AOF文件,fsync完成後進程程返回
AOF_FSYNC_EVERYSEC 命令寫入aof_buf後調用系統write操作,write完成後線程返回。fsync同步文件操作由進程每秒調用一次
AOF_FSYNC_NO 命令寫入aof_buf後調用系統write操作,不對AOF文件做fsync同步,同步硬盤由操作由操作系統負責

我們來了解一下,write和fsync操作,在系統中都做了哪些事:

  • write操作:會觸發延遲寫(delayed write)機制。Linux在內核提供頁緩衝區用來提高IO性能,因此,write操作在將數據寫入操作系統的緩衝區後就直接返回,而不一定觸發同步到磁盤的操作。只有在頁空間寫滿,或者達到特定的時間週期,纔會同步到磁盤。因此單純的write操作也是有數據丟失的風險。
  • fsync操作:針對單個文件操作,做強制硬盤同步,fsync將阻塞直到寫入硬盤完成後返回。

雖然Redis提供了三種同步策略,兼顧安全和性能的同步策略是:AOF_FSYNC_EVERYSEC。但是仍有丟失數據的風險,而且不是一秒而是兩秒的數據,接下來就看同步的源碼實現:

// 將AOF緩存寫到磁盤中
// 因爲我們需要在回覆client之前對AOF執行寫操作,唯一的機會是在事件loop中,因此累計所有的AOF到緩存中,在下一次重新進入事件loop之前將緩存寫到AOF文件中

// 關於force參數
// 當fsync被設置爲每秒執行一次,如果後臺仍有線程正在執行fsync操作,我們可能會延遲flush操作,因爲write操作可能會被阻塞,當發生這種情況時,說明需要儘快的執行flush操作,會調用 serverCron() 函數。
// 然而如果force被設置爲1,我們會無視後臺的fsync,直接進行寫入操作

#define AOF_WRITE_LOG_ERROR_RATE 30 /* Seconds between errors logging. */
// 將AOF緩存沖洗到磁盤中
void flushAppendOnlyFile(int force) {
    ssize_t nwritten;
    int sync_in_progress = 0;
    mstime_t latency;

    // 如果緩衝區中沒有數據,直接返回
    if (sdslen(server.aof_buf) == 0) return;

    // 同步策略是每秒同步一次
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
        // AOF同步操作是否在後臺正在運行
        sync_in_progress = bioPendingJobsOfType(BIO_AOF_FSYNC) != 0;

    // 同步策略是每秒同步一次,且不是強制同步的
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
        /* With this append fsync policy we do background fsyncing.
         * If the fsync is still in progress we can try to delay
         * the write for a couple of seconds. */
        // 根據這個同步策略,且沒有設置強制執行,我們在後臺執行同步
        // 如果同步已經在後臺執行,那麼可以延遲兩秒,如果設置了force,那麼服務器會阻塞在write操作上

        // 如果後臺正在執行同步
        if (sync_in_progress) {
            // 延遲執行flush操作的開始時間爲0,表示之前沒有延遲過write
            if (server.aof_flush_postponed_start == 0) {
                /* No previous write postponing, remember that we are
                 * postponing the flush and return. */
                // 之前沒有延遲過write操作,那麼將延遲write操作的開始時間保存下來,然後就直接返回
                server.aof_flush_postponed_start = server.unixtime;
                return;
            // 如果之前延遲過write操作,如果沒到2秒,直接返回,不執行write
            } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
                /* We were already waiting for fsync to finish, but for less
                 * than two seconds this is still ok. Postpone again. */
                return;
            }
            /* Otherwise fall trough, and go write since we can't wait
             * over two seconds. */
            // 執行到這裏,表示後臺正在執行fsync,但是延遲時間已經超過2秒
            // 那麼執行write操作,此時write會被阻塞
            server.aof_delayed_fsync++;
            serverLog(LL_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.");
        }
    }
    /* We want to perform a single write. This should be guaranteed atomic
     * at least if the filesystem we are writing is a real physical one.
     * While this will save us against the server being killed I don't think
     * there is much to do about the whole server stopping for power problems
     * or alike */
    // 執行write操作,保證寫操作是原子操作

    // 設置延遲檢測開始的時間
    latencyStartMonitor(latency);
    // 將緩衝區的內容寫到AOF文件中
    nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
    // 設置延遲的時間 = 當前的時間 - 開始的時間
    latencyEndMonitor(latency);
    /* We want to capture different events for delayed writes:
     * when the delay happens with a pending fsync, or with a saving child
     * active, and when the above two conditions are missing.
     * We also use an additional event name to save all samples which is
     * useful for graphing / monitoring purposes. */
    // 捕獲不同造成延遲write的事件
    // 如果正在後臺執行同步fsync
    if (sync_in_progress) {
        // 將latency和"aof-write-pending-fsync"關聯到延遲診斷字典中
        latencyAddSampleIfNeeded("aof-write-pending-fsync",latency);
    // 如果正在執行AOF或正在執行RDB
    } else if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) {
        // 將latency和"aof-write-active-child"關聯到延遲診斷字典中
        latencyAddSampleIfNeeded("aof-write-active-child",latency);
    } else {
        // 將latency和"aof-write-alone"關聯到延遲診斷字典中
        latencyAddSampleIfNeeded("aof-write-alone",latency);
    }
    // 將latency和"aof-write"關聯到延遲診斷字典中
    latencyAddSampleIfNeeded("aof-write",latency);

    /* We performed the write so reset the postponed flush sentinel to zero. */
    // 執行了write,所以清零延遲flush的時間
    server.aof_flush_postponed_start = 0;

    // 如果寫入的字節數不等於緩存的字節數,發生異常錯誤
    if (nwritten != (signed)sdslen(server.aof_buf)) {
        static time_t last_write_error_log = 0;
        int can_log = 0;

        /* Limit logging rate to 1 line per AOF_WRITE_LOG_ERROR_RATE seconds. */
        // 限制日誌的頻率每行30秒
        if ((server.unixtime - last_write_error_log) > AOF_WRITE_LOG_ERROR_RATE) {
            can_log = 1;
            last_write_error_log = server.unixtime;
        }

        /* Log the AOF write error and record the error code. */
        // 如果寫入錯誤,寫errno到日誌
        if (nwritten == -1) {
            if (can_log) {
                serverLog(LL_WARNING,"Error writing to the AOF file: %s",
                    strerror(errno));
                server.aof_last_write_errno = errno;
            }
        // 如果是寫了一部分,發生錯誤
        } else {
            if (can_log) {
                serverLog(LL_WARNING,"Short write while writing to "
                                       "the AOF file: (nwritten=%lld, "
                                       "expected=%lld)",
                                       (long long)nwritten,
                                       (long long)sdslen(server.aof_buf));
            }

            // 將追加的內容截斷,刪除了追加的內容,恢復成原來的文件
            if (ftruncate(server.aof_fd, server.aof_current_size) == -1) {
                if (can_log) {
                    serverLog(LL_WARNING, "Could not remove short write "
                             "from the append-only file.  Redis may refuse "
                             "to load the AOF the next time it starts.  "
                             "ftruncate: %s", strerror(errno));
                }
            } else {
                /* If the ftruncate() succeeded we can set nwritten to
                 * -1 since there is no longer partial data into the AOF. */
                nwritten = -1;
            }
            server.aof_last_write_errno = ENOSPC;
        }

        /* Handle the AOF write error. */
        // 如果是寫入的策略爲每次寫入就同步,無法恢復這種策略的寫,因爲我們已經告知使用者,已經將寫的數據同步到磁盤了,因此直接退出程序
        if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
            /* We can't recover when the fsync policy is ALWAYS since the
             * reply for the client is already in the output buffers, and we
             * have the contract with the user that on acknowledged write data
             * is synced on disk. */
            serverLog(LL_WARNING,"Can't recover from AOF write error when the AOF fsync policy is 'always'. Exiting...");
            exit(1);
        } else {
            /* Recover from failed write leaving data into the buffer. However
             * set an error to stop accepting writes as long as the error
             * condition is not cleared. */
            //設置執行write操作的狀態
            server.aof_last_write_status = C_ERR;

            /* Trim the sds buffer if there was a partial write, and there
             * was no way to undo it with ftruncate(2). */
            // 如果只寫入了局部,沒有辦法用ftruncate()函數去恢復原來的AOF文件
            if (nwritten > 0) {
                // 只能更新當前的AOF文件的大小
                server.aof_current_size += nwritten;
                // 刪除AOF緩衝區寫入的字節數
                sdsrange(server.aof_buf,nwritten,-1);
            }
            return; /* We'll try again on the next call... */
        }

    // nwritten == (signed)sdslen(server.aof_buf
    // 執行write寫入成功
    } else {
        /* Successful write(2). If AOF was in error state, restore the
         * OK state and log the event. */
        // 更新最近一次寫的狀態爲 C_OK
        if (server.aof_last_write_status == C_ERR) {
            serverLog(LL_WARNING,
                "AOF write error looks solved, Redis can write again.");
            server.aof_last_write_status = C_OK;
        }
    }
    // 只能更新當前的AOF文件的大小
    server.aof_current_size += nwritten;

    /* Re-use AOF buffer when it is small enough. The maximum comes from the
     * arena size of 4k minus some overhead (but is otherwise arbitrary). */
    // 如果這個緩存足夠小,小於4K,那麼重用這個緩存,否則釋放AOF緩存
    if ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) {
        sdsclear(server.aof_buf);   //將緩存內容清空,重用
    } else {
        sdsfree(server.aof_buf);    //釋放緩存空間
        server.aof_buf = sdsempty();//創建一個新緩存
    }

    /* Don't fsync if no-appendfsync-on-rewrite is set to yes and there are
     * children doing I/O in the background. */
    // 如果no-appendfsync-on-rewrite被設置爲yes,表示正在執行重寫,則不執行fsync
    // 或者正在執行 BGSAVE 或 BGWRITEAOF,也不執行
    if (server.aof_no_fsync_on_rewrite &&
        (server.aof_child_pid != -1 || server.rdb_child_pid != -1))
            return;

    /* Perform the fsync if needed. */

    // 執行fsync進行同步,每次寫入都同步
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
        /* aof_fsync is defined as fdatasync() for Linux in order to avoid
         * flushing metadata. */
        // 設置延遲檢測開始的時間
        latencyStartMonitor(latency);
        // Linux下調用fdatasync()函數更高效的執行同步
        aof_fsync(server.aof_fd); /* Let's try to get this data on the disk */
        // 設置延遲的時間 = 當前的時間 - 開始的時間
        latencyEndMonitor(latency);
        // 將latency和"aof-fsync-always"關聯到延遲診斷字典中
        latencyAddSampleIfNeeded("aof-fsync-always",latency);
        // 更新最近一次執行同步的時間
        server.aof_last_fsync = server.unixtime;

    // 每秒執行一次同步,當前時間大於上一次執行同步的時間
    } else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
                server.unixtime > server.aof_last_fsync)) {
        // 如果沒有正在執行同步,那麼在後臺開一個線程執行同步
        if (!sync_in_progress) aof_background_fsync(server.aof_fd);
        // 更新最近一次執行同步的時間
        server.aof_last_fsync = server.unixtime;
    }
}

2.2 重寫機制

當一個數據庫的命令非常多時,AOF文件就會非常大,爲了解決這個問題,Redis引入了AOF重寫機制來壓縮文件的體積。

Redis AOF持久化機制源碼註釋

2.2.1 AOF重寫的方式

  • 進程內已經超時的數據不在寫入文件。
  • 無效命令不在寫入文件。
  • 多條寫的命令合併成一個。

總之,AOF總是記錄數據庫的最終狀態的一個命令集。類似於物理中的位移與路程的關係,位移總是關心的是啓動到終點距離,而不關心是如何從起點到達終點。

2.2.2 觸發機制

  • 手動觸發:BGREWRITEAOF 命令。
  • 自動觸發:根據redis.conf的兩個參數確定觸發的時機。
    • auto-aof-rewrite-percentage 100:當前AOF的文件空間(aof_current_size)和上一次重寫後AOF文件空間(aof_base_size)的比值。
    • auto-aof-rewrite-min-size 64mb:表示運行AOF重寫時文件最小的體積。
    • 自動觸發時機 = (aof_current_size > auto-aof-rewrite-min-size && (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage)

2.2.3 AOF重寫的實現

AOF重寫操作有可能會長時間阻塞服務器主進程,因此會fork()一個子進程在後臺進行重寫,然後父進程就可以繼續響應命令請求。雖然解決了阻塞問題,但是有產生了新問題:子進程在重寫期間,服務其還會處理新的命令請求,而這些命令可能灰度數據庫的狀態進行更改,從而使當前的數據庫狀態和AOF重寫之後保存的狀態不一致。

因此Redis設置了一個AOF重寫緩衝區的結構。

// AOF緩衝區大小
#define AOF_RW_BUF_BLOCK_SIZE (1024*1024*10)    /* 10 MB per block */

// AOF塊緩衝區結構
typedef struct aofrwblock {
    // 當前已經使用的和可用的字節數
    unsigned long used, free;
    // 緩衝區
    char buf[AOF_RW_BUF_BLOCK_SIZE];
} aofrwblock;

重寫緩衝區並不是一個大塊的內存空間,而是一些內存塊的鏈表,沒個內存塊的大小爲10MB,這樣就組成了一個重寫緩衝區。

因此當客戶端發來命令時,會執行以下操作:

  1. 執行客戶端的命令。
  2. 將執行後的寫命令追加到AOF緩衝區(server.aof_buf)中。
  3. 將執行後的寫命令追加到AOF重寫緩衝區(server.aof_rewrite_buf_blocks)中。

這樣以來就不會丟失子進程重寫期間,父進程新處理的寫命令了。

於是,我們查看一下後臺執行重寫操作的源碼。

// 以下是BGREWRITEAOF的工作步驟
// 1. 用戶調用BGREWRITEAOF
// 2. Redis調用這個函數,它執行fork()
//      2.1 子進程在臨時文件中執行重寫操作
//      2.2 父進程將累計的差異數據追加到server.aof_rewrite_buf中
// 3. 當子進程完成2.1
// 4. 父進程會捕捉到子進程的退出碼,如果是OK,那麼追加累計的差異數據到臨時文件,並且對臨時文件rename,用它代替舊的AOF文件,然後就完成AOF的重寫。
int rewriteAppendOnlyFileBackground(void) {
    pid_t childpid;
    long long start;

    // 如果正在進行重寫或正在進行RDB持久化操作,則返回C_ERR
    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
    // 創建父子進程間通信的管道
    if (aofCreatePipes() != C_OK) return C_ERR;
    // 記錄fork()開始時間
    start = ustime();

    // 子進程
    if ((childpid = fork()) == 0) {
        char tmpfile[256];

        /* Child */
        // 關閉監聽的套接字
        closeListeningSockets(0);
        // 設置進程名字
        redisSetProcTitle("redis-aof-rewrite");
        // 創建臨時文件
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        // 對臨時文件進行AOF重寫
        if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
            // 獲取子進程使用的內存空間大小
            size_t private_dirty = zmalloc_get_private_dirty();

            if (private_dirty) {
                serverLog(LL_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;
        // 計算fork的速率,GB/每秒
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
        // 將"fork"和fork消耗的時間關聯到延遲診斷字典中
        latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
        if (childpid == -1) {
            serverLog(LL_WARNING,
                "Can't rewrite append only file in background: fork: %s",
                strerror(errno));
            return C_ERR;
        }
        // 打印日誌
        serverLog(LL_NOTICE,
            "Background append only file rewriting started by pid %d",childpid);
        // 將AOF日程標誌清零
        server.aof_rewrite_scheduled = 0;
        // AOF開始的時間
        server.aof_rewrite_time_start = time(NULL);
        // 設置AOF重寫的子進程pid
        server.aof_child_pid = childpid;
        // 在AOF或RDB期間,不能對哈希表進行resize操作
        updateDictResizePolicy();
        // 將aof_selected_db設置爲-1,強制讓feedAppendOnlyFile函數執行時,執行一個select命令
        server.aof_selected_db = -1;
        // 清空腳本緩存
        replicationScriptCacheFlush();
        return C_OK;
    }
    return C_OK; /* unreached */
}

服務器主進程執行了fork操作生成一個子進程執行rewriteAppendOnlyFile()函數進行對臨時文件的重寫操作。

rewriteAppendOnlyFile()函數源碼如下:

// 寫一系列的命令,用來完全重建數據集到filename文件中,被 REWRITEAOF and BGREWRITEAOF調用
// 爲了使重建數據集的命令數量最小,Redis會使用 可變參的命令,例如RPUSH, SADD 和 ZADD。
// 然而每次單個命令的元素數量不能超過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();
    char byte;
    size_t processed = 0;

    // 創建臨時文件的名字保存到tmpfile中
    snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
    // 打開文件
    fp = fopen(tmpfile,"w");
    if (!fp) {
        serverLog(LL_WARNING, "Opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): %s", strerror(errno));
        return C_ERR;
    }
    // 設置一個空sds給 保存子進程AOF時差異累計數據的sds
    server.aof_child_diff = sdsempty();
    // 初始化rio爲文件io對象
    rioInitWithFile(&aof,fp);
    // 如果開啓了增量時同步,防止在緩存中累計太多命令,造成寫入時IO阻塞時間過長
    if (server.aof_rewrite_incremental_fsync)
        // 設置自動同步的字節數限制爲AOF_AUTOSYNC_BYTES = 32MB
        rioSetAutoSync(&aof,AOF_AUTOSYNC_BYTES);

    // 遍歷所有的數據庫
    for (j = 0; j < server.dbnum; j++) {
        // 按照格式構建 SELECT 命令內容
        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) {
            // 創建失敗返回C_ERR
            fclose(fp);
            return C_ERR;
        }

        // 將SELECT 命令寫入AOF文件,確保後面的命令能正確載入到數據庫
        if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
        // 將數據庫的ID吸入AOF文件
        if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;

        // 遍歷保存當前數據的鍵值對的字典
        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 (expiretime != -1 && expiretime < now) continue;

            // 根據值的對象類型,將鍵值對寫到AOF文件中

            // 值爲字符串類型對象
            if (o->type == OBJ_STRING) {
                char cmd[]="*3\r\n$3\r\nSET\r\n";
                // 按格式寫入SET命令
                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 == OBJ_LIST) {
                // 重建一個列表對象命令,將鍵值對按格式寫入
                if (rewriteListObject(&aof,&key,o) == 0) goto werr;
            // 值爲集合類型對象
            } else if (o->type == OBJ_SET) {
                // 重建一個集合對象命令,將鍵值對按格式寫入
                if (rewriteSetObject(&aof,&key,o) == 0) goto werr;
            // 值爲有序集合類型對象
            } else if (o->type == OBJ_ZSET) {
                // 重建一個有序集合對象命令,將鍵值對按格式寫入
                if (rewriteSortedSetObject(&aof,&key,o) == 0) goto werr;
            // 值爲哈希類型對象
            } else if (o->type == OBJ_HASH) {
                // 重建一個哈希對象命令,將鍵值對按格式寫入
                if (rewriteHashObject(&aof,&key,o) == 0) goto werr;
            } else {
                serverPanic("Unknown object type");
            }
            // 如果該鍵有過期時間,且沒過期,寫入過期時間
            if (expiretime != -1) {
                char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";
                // 將過期鍵時間全都以Unix時間寫入
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                if (rioWriteBulkLongLong(&aof,expiretime) == 0) goto werr;
            }
            // 在rio的緩存中每次寫了10M,就從父進程讀累計的差異,保存到子進程的aof_child_diff中
            if (aof.processed_bytes > processed+1024*10) {
                // 更新已寫的字節數
                processed = aof.processed_bytes;
                // 從父進程讀累計寫入的緩衝區的差異,在重寫結束時鏈接到文件的結尾
                aofReadDiffFromParent();
            }
        }
        dictReleaseIterator(di);    //釋放字典迭代器
        di = NULL;
    }

    // 當父進程仍然在發送數據時,先執行一個緩慢的同步,以便下一次最中的同步更快
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;

    // 再次從父進程讀取幾次數據,以獲得更多的數據,我們無法一直讀取,因爲服務器從client接受的數據總是比發送給子進程要快,所以當數據來臨的時候,我們嘗試從在循環中多次讀取。
    // 如果在20ms之內沒有新的數據到來,那麼我們終止讀取
    int nodata = 0;
    mstime_t start = mstime();  //讀取的開始時間
    // 在20ms之內等待數據到來
    while(mstime()-start < 1000 && nodata < 20) {
        // 在1ms之內,查看從父進程讀數據的fd是否變成可讀的,若不可讀則aeWait()函數返回0
        if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
        {
            nodata++;   //更新新數據到來的時間,超過20ms則退出while循環
            continue;
        }
        // 當管道的讀端可讀時,清零nodata
        nodata = 0; /* Start counting from zero, we stop on N *contiguous* timeouts. */
        // 從父進程讀累計寫入的緩衝區的差異,在重寫結束時鏈接到文件的結尾
        aofReadDiffFromParent();
    }

    // 請求父進程停止發送累計差異數據
    if (write(server.aof_pipe_write_ack_to_parent,"!",1) != 1) goto werr;
    // 將從父進程讀ack的fd設置爲非阻塞模式
    if (anetNonBlock(NULL,server.aof_pipe_read_ack_from_parent) != ANET_OK)
        goto werr;
    // 在5000ms之內,從fd讀1個字節的數據保存在byte中,查看byte是否是'!'
    if (syncRead(server.aof_pipe_read_ack_from_parent,&byte,1,5000) != 1 ||
        byte != '!') goto werr;
    // 如果收到的是父進程發來的'!',則打印日誌
    serverLog(LL_NOTICE,"Parent agreed to stop sending diffs. Finalizing AOF...");

    // 最後一次從父進程讀累計寫入的緩衝區的差異
    aofReadDiffFromParent();

    serverLog(LL_NOTICE,
        "Concatenating %.2f MB of AOF diff received from parent.",
        (double) sdslen(server.aof_child_diff) / (1024*1024));
    // 將子進程aof_child_diff中保存的差異數據寫到AOF文件中
    if (rioWrite(&aof,server.aof_child_diff,sdslen(server.aof_child_diff)) == 0)
        goto werr;

    // 再次沖洗文件緩衝區,執行同步操作
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;   //關閉文件

    // 原子性的將臨時文件的名字,改成appendonly.aof
    if (rename(tmpfile,filename) == -1) {
        serverLog(LL_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        return C_ERR;
    }
    // 打印日誌
    serverLog(LL_NOTICE,"SYNC append only file rewrite performed");
    return C_OK;

// 寫錯誤處理
werr:
    serverLog(LL_WARNING,"Write error writing append only file on disk: %s", strerror(errno));
    fclose(fp);
    unlink(tmpfile);
    if (di) dictReleaseIterator(di);
    return C_ERR;
}

我們可以看到在關閉文件之前,多次執行了從重寫緩衝區做讀操作的aofReadDiffFromParent()。在最後執行了rioWrite(&aof,server.aof_child_diff,sdslen(server.aof_child_diff)操作,這就是把AOF重寫緩衝區保存服務器主進程新命令追加寫到AOF文件中,以此保證了AOF文件的數據狀態和數據庫的狀態一致。

2.3 父子進程間的通信

整個重寫的過程中,父子進行通信的地方只有一個,那就是最後父進程在子進程做重寫操作完成時,把子進程重寫操作期間所執行的新命令發送給子進程的重寫緩衝區,子進程然後將重寫緩衝區的數據追加到AOF文件中。

Redis AOF持久化機制源碼註釋

而父進程是如何將差異數據發送給子進程呢?Redis中使用了管道技術進程間通信(IPC)之管道詳解

在上文提到的rewriteAppendOnlyFileBackground()函數首先就創建了父子通信的管道。

父子進程間通信時共創建了三組管道

//下面兩個是發送差異數據管道
int aof_pipe_write_data_to_child;   //父進程寫給子進程的文件描述符
int aof_pipe_read_data_from_parent; //子進程從父進程讀的文件描述符

//下面四個是應答ack的管道
int aof_pipe_write_ack_to_parent;   //子進程寫ack給父進程的文件描述符
int aof_pipe_read_ack_from_child;   //父進程從子進程讀ack的文件描述符
int aof_pipe_write_ack_to_child;    //父進程寫ack給子進程的文件描述符
int aof_pipe_read_ack_from_parent;  //子進程從父進程讀ack的文件描述符

當將feedAppendOnlyFile()將命令追加到緩衝區的同時,還在最後調用了aofRewriteBufferAppend()函數,這個函數就是將命令追加到AOF的緩衝區,然而,在追加完成後會執行這麼一段代碼

// 獲取當前事件正在監聽的類型,如果等於0,未設置,則設置管道aof_pipe_write_data_to_child爲可寫狀態
// 當然aof_pipe_write_data_to_child可以用的時候,調用aofChildWriteDiffDatah()函數寫數據
if (aeGetFileEvents(server.el,server.aof_pipe_write_data_to_child) == 0) {
    aeCreateFileEvent(server.el, server.aof_pipe_write_data_to_child,
    AE_WRITABLE, aofChildWriteDiffData, NULL);
}

當然aof_pipe_write_data_to_child可以寫的時候,調用aofChildWriteDiffDatah()函數寫數據,而在aofChildWriteDiffDatah()函數中,則將重寫緩衝區數據寫到管道中。函數源碼如下:

// 事件處理程序發送一些數據給正在做AOF重寫的子進程,我們發送AOF緩衝區一部分不同的數據給子進程,當子進程完成重寫時,重寫的文件會比較小
void aofChildWriteDiffData(aeEventLoop *el, int fd, void *privdata, int mask) {
    listNode *ln;
    aofrwblock *block;
    ssize_t nwritten;
    UNUSED(el);
    UNUSED(fd);
    UNUSED(privdata);
    UNUSED(mask);

    while(1) {
        // 獲取緩衝塊鏈表的頭節點地址
        ln = listFirst(server.aof_rewrite_buf_blocks);
        // 獲取緩衝塊地址
        block = ln ? ln->value : NULL;
        // 如果aof_stop_sending_diff爲真,則停止發送累計的不同數據給子進程,或者緩衝塊爲空
        // 則將管道的寫端從服務器的監聽隊列中刪除
        if (server.aof_stop_sending_diff || !block) {
            aeDeleteFileEvent(server.el,server.aof_pipe_write_data_to_child,
                              AE_WRITABLE);
            return;
        }
        // 如果已經有緩存的數據
        if (block->used > 0) {
            // 則將緩存的數據寫到管道中
            nwritten = write(server.aof_pipe_write_data_to_child,
                             block->buf,block->used);
            if (nwritten <= 0) return;
            // 更新緩衝區的數據,覆蓋掉已經寫到管道的數據
            memmove(block->buf,block->buf+nwritten,block->used-nwritten);
            block->used -= nwritten;
        }
        // 如果當前節點的所緩衝的數據全部寫完,則刪除該節點
        if (block->used == 0) listDelNode(server.aof_rewrite_buf_blocks,ln);
    }
}

而在上面展示到的rewriteAppendOnlyFile()函數中,則當aof_pipe_read_data_from_parent可讀時,不斷調用aofReadDiffFromParent()函數的從管道讀數據,這樣就實現了父子進程的通信。該函數源碼如下:

// 該函數在子進程正在進行重寫AOF文件時調用
// 用來讀從父進程累計寫入的緩衝區的差異,在重寫結束時鏈接到文件的結尾
ssize_t aofReadDiffFromParent(void) {
    // 大多數Linux系統中默認的管道大小
    char buf[65536]; /* Default pipe buffer size on most Linux systems. */
    ssize_t nread, total = 0;

    // 從父進程讀數據到buf中,讀了nread個字節
    while ((nread =
            read(server.aof_pipe_read_data_from_parent,buf,sizeof(buf))) > 0) {
        // 將buf中的數據累計到子進程的差異累計的sds中
        server.aof_child_diff = sdscatlen(server.aof_child_diff,buf,nread);
        // 更新總的累計字節數
        total += nread;
    }
    return total;
}

從中可以看到,子進程從管道讀的數據全部保存在server.aof_child_diff中。

2.4 AOF文件的載入

因爲Redis命令總是在一個客戶端中執行,因此,爲了載入AOF文件,需要創建一個關閉監聽套接字的僞客戶端。AOF文件的載入和寫入是相反的過程,因此比較簡單,直接給出註釋的源碼:Redis AOF持久化機制源碼註釋

// 執行AOF文件中的命令
// 成功返回C_OK,出現非致命錯誤返回C_ERR,例如AOF文件長度爲0,出現致命錯誤打印日誌退出
int loadAppendOnlyFile(char *filename) {
    struct client *fakeClient;
    FILE *fp = fopen(filename,"r"); //以讀打開AOF文件
    struct redis_stat sb;
    int old_aof_state = server.aof_state;   //備份當前AOF的狀態
    long loops = 0;
    off_t valid_up_to = 0; /* Offset of the latest well-formed command loaded. */

    // 如果文件打開,但是大小爲0,則返回C_ERR
    if (fp && redis_fstat(fileno(fp),&sb) != -1 && sb.st_size == 0) {
        server.aof_current_size = 0;
        fclose(fp);
        return C_ERR;
    }

    // 如果文件打開失敗,打印日誌,退出
    if (fp == NULL) {
        serverLog(LL_WARNING,"Fatal error: can't open the append log file for reading: %s",strerror(errno));
        exit(1);
    }

    /* Temporarily disable AOF, to prevent EXEC from feeding a MULTI
     * to the same file we're about to read. */
    // 暫時關閉AOF,防止在執行MULTI時,EXEC命令被傳播到AOF文件中
    server.aof_state = AOF_OFF;

    // 生成一個僞client
    fakeClient = createFakeClient();
    // 設置載入的狀態信息
    startLoading(fp);

    while(1) {
        int argc, j;
        unsigned long len;
        robj **argv;
        char buf[128];
        sds argsds;
        struct redisCommand *cmd;

        /* Serve the clients from time to time */
        // 間隔性的處理client請求
        if (!(loops++ % 1000)) {
            // ftello(fp)返回當前文件載入的偏移量
            // 設置載入時server的狀態信息,更新當前載入的進度
            loadingProgress(ftello(fp));
            // 在服務器被阻塞的狀態下,仍然能處理請求
            // 因爲當前處於載入狀態,當client的請求到來時,總是返回loading的狀態錯誤
            processEventsWhileBlocked();
        }

        // 將一行文件內容讀到buf中,遇到"\r\n"停止
        if (fgets(buf,sizeof(buf),fp) == NULL) {
            if (feof(fp))   //如果文件已經讀完了或數據庫爲空,則跳出while循環
                break;
            else
                goto readerr;
        }
        // 檢查文件格式 "*<argc>\r\n"
        if (buf[0] != '*') goto fmterr;
        if (buf[1] == '\0') goto readerr;
        // 取出命令參數個數
        argc = atoi(buf+1);
        if (argc < 1) goto fmterr;  //至少一個參數,就是當前命令

        // 分配參數列表空間
        argv = zmalloc(sizeof(robj*)*argc);
        // 設置僞client的參數列表
        fakeClient->argc = argc;
        fakeClient->argv = argv;

        // 遍歷參數列表
        // "$<command_len>\r\n<command>\r\n"
        for (j = 0; j < argc; j++) {
            // 讀一行內容到buf中,遇到"\r\n"停止
            if (fgets(buf,sizeof(buf),fp) == NULL) {
                fakeClient->argc = j; /* Free up to j-1. */
                freeFakeClientArgv(fakeClient);
                goto readerr;
            }
            // 檢查格式
            if (buf[0] != '$') goto fmterr;
            // 讀出參數的長度len
            len = strtol(buf+1,NULL,10);
            // 初始化一個len長度的sds
            argsds = sdsnewlen(NULL,len);
            // 從文件中讀出一個len字節長度,將值保存到argsds中
            if (len && fread(argsds,len,1,fp) == 0) {
                sdsfree(argsds);
                fakeClient->argc = j; /* Free up to j-1. */
                freeFakeClientArgv(fakeClient);
                goto readerr;
            }
            // 創建一個字符串對象保存讀出的參數argsds
            argv[j] = createObject(OBJ_STRING,argsds);
            // 讀兩個字節,跳過"\r\n"
            if (fread(buf,2,1,fp) == 0) {
                fakeClient->argc = j+1; /* Free up to j. */
                freeFakeClientArgv(fakeClient);
                goto readerr; /* discard CRLF */
            }
        }

        /* Command lookup */
        // 查找命令
        cmd = lookupCommand(argv[0]->ptr);
        if (!cmd) {
            serverLog(LL_WARNING,"Unknown command '%s' reading the append only file", (char*)argv[0]->ptr);
            exit(1);
        }

        /* Run the command in the context of a fake client */
        // 調用僞client執行命令
        cmd->proc(fakeClient);

        /* The fake client should not have a reply */
        // 僞client不應該有回覆
        serverAssert(fakeClient->bufpos == 0 && listLength(fakeClient->reply) == 0);
        /* The fake client should never get blocked */
        // 僞client不應該是阻塞的
        serverAssert((fakeClient->flags & CLIENT_BLOCKED) == 0);

        /* Clean up. Command code may have changed argv/argc so we use the
         * argv/argc of the client instead of the local variables. */
        // 釋放僞client的參數列表
        freeFakeClientArgv(fakeClient);
        // 更新已載入且命令合法的當前文件的偏移量
        if (server.aof_load_truncated) valid_up_to = ftello(fp);
    }

    /* This point can only be reached when EOF is reached without errors.
     * If the client is in the middle of a MULTI/EXEC, log error and quit. */
    // 執行到這裏,說明AOF文件的所有內容都被正確的讀取
    // 如果僞client處於 MULTI/EXEC 的環境中,還有檢測文件是否包含正確事物的結束,調到uxeof
    if (fakeClient->flags & CLIENT_MULTI) goto uxeof;

// 載入成功
loaded_ok: /* DB loaded, cleanup and return C_OK to the caller. */
    fclose(fp); //關閉文件
    freeFakeClient(fakeClient); //釋放僞client
    server.aof_state = old_aof_state;   //還原AOF狀態
    stopLoading();  //設置載入完成的狀態
    aofUpdateCurrentSize(); //更新服務器狀態,當前AOF文件的大小
    server.aof_rewrite_base_size = server.aof_current_size; //更新重寫的大小
    return C_OK;

// 載入時讀錯誤,如果feof(fp)爲真,則直接執行 uxeof
readerr: /* Read error. If feof(fp) is true, fall through to unexpected EOF. */
    if (!feof(fp)) {
        // 退出前釋放僞client的空間
        if (fakeClient) freeFakeClient(fakeClient); /* avoid valgrind warning */
        serverLog(LL_WARNING,"Unrecoverable error reading the append only file: %s", strerror(errno));
        exit(1);
    }

// 不被預期的AOF文件結束格式
uxeof: /* Unexpected AOF end of file. */
    // 如果發現末尾結束格式不完整則自動截掉,成功加載前面正確的數據。
    if (server.aof_load_truncated) {
        serverLog(LL_WARNING,"!!! Warning: short read while loading the AOF file !!!");
        serverLog(LL_WARNING,"!!! Truncating the AOF at offset %llu !!!",
            (unsigned long long) valid_up_to);
        // 截斷文件到正確加載的位置
        if (valid_up_to == -1 || truncate(filename,valid_up_to) == -1) {
            if (valid_up_to == -1) {
                serverLog(LL_WARNING,"Last valid command offset is invalid");
            } else {
                serverLog(LL_WARNING,"Error truncating the AOF file: %s",
                    strerror(errno));
            }
        } else {
            /* Make sure the AOF file descriptor points to the end of the
             * file after the truncate call. */
            // 確保截斷後的文件指針指向文件的末尾
            if (server.aof_fd != -1 && lseek(server.aof_fd,0,SEEK_END) == -1) {
                serverLog(LL_WARNING,"Can't seek the end of the AOF file: %s",
                    strerror(errno));
            } else {
                serverLog(LL_WARNING,
                    "AOF loaded anyway because aof-load-truncated is enabled");
                goto loaded_ok; //跳轉到loaded_ok,表截斷成功,成功加載前面正確的數據。
            }
        }
    }
    // 退出前釋放僞client的空間
    if (fakeClient) freeFakeClient(fakeClient); /* avoid valgrind warning */
    serverLog(LL_WARNING,"Unexpected end of file reading the append only file. You can: 1) Make a backup of your AOF file, then use ./redis-check-aof --fix <filename>. 2) Alternatively you can set the 'aof-load-truncated' configuration option to yes and restart the server.");
    exit(1);

// 格式錯誤
fmterr: /* Format error. */
    // 退出前釋放僞client的空間
    if (fakeClient) freeFakeClient(fakeClient); /* avoid valgrind warning */
    serverLog(LL_WARNING,"Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>");
    exit(1);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章