redis學習筆記——RDB和AOF持久化一

爲防止數據丟失,須要將 Redis 中的數據從內存中 dump 到磁盤,這就是持久化。Redis 提供兩種持久化方式:RDB 和 AOF。Redis 容許二者結合,也容許二者同時關閉。html

RDB 能夠定時備分內存中的數據集。服務器啓動的時候,能夠從 RDB 文件中恢復數據集。redis

AOF(append only file) 能夠記錄服務器的全部寫操做。在服務器從新啓動的時候,會把全部的寫操做從新執行一遍,從而實現數據備份。當寫操做集過大(比原有的數據集還大),Redis 會重寫寫操做集。數據庫

值得一提的是,由於AOF文件的更新頻率一般比RDB文件的更新頻率高,因此若是服務器開啓了AOF持久化功能,那麼服務器會優先使用AOF文件來還原數據庫狀態。只有在AOF持久化功能處於關閉狀態時,服務器纔會使用RDB文件來還原數據庫狀態。數組

RDB持久化

RDB文件的建立和載入

有兩個Redis命令能夠用於生成RDB文件,一個是SAVE,另外一個是BGSAVE。緩存

二者區別:服務器

  • SAVE命令會阻塞Redis服務器進程,直到RDB文件建立完畢爲止,在服務器進程阻塞期間,服務器不能處理任何命令請求;
  • BGSAVE命令會派生出一個子進程,而後由子進程負責建立RDB文件,服務器進程(父進程)繼續處理命令請求;

建立RDB文件的實際工做由rdb.c/rdbSave函數完成,SAVE命令和BGSAVE命令會以不一樣的方式調用這個函數,經過如下僞代碼能夠明顯地看出這兩個命令之間的區別:網絡

def SAVE():
    # 
建立RDB
文件
    rdbSave()
def BGSAVE():
    # 
建立子進程
    pid = fork()
    if pid == 0:
        # 
子進程負責建立RDB
文件
        rdbSave()
        # 
完成以後向父進程發送信號
        signal_parent()
    elif pid 0:
        # 
父進程繼續處理命令請求,並經過輪詢等待子進程的信號
        handle_request_and_wait_signal()
    else:
        # 
處理出錯狀況
        handle_fork_error()

 

載入RDB文件的實際工做由rdb.c/rdbLoad函數完成,這個函數和rdbSave函數之間的關係能夠用下圖表示:
數據結構

服務器在載入RDB文件期間,會一直處於阻塞狀態,直到載入工做完成爲止。app

自動間隔性保存

用戶能夠經過save選項設置多個保存條件,但只要其中任意一個條件被知足,服務器就會執行BGSAVE命令。函數

舉個例子,若是咱們向服務器提供如下配置:

save 900 1
save 300 10
save 60 10000
那麼只要知足如下三個條件中的任意一個,BGSAVE命令就會被執行:
·服務器在900秒以內,對數據庫進行了至少1次修改;

·服務器在300秒以內,對數據庫進行了至少10次修改;

·服務器在60秒以內,對數據庫進行了至少10000次修改。

 

用戶設置完save選項後(或者系統默認),接着,服務器程序會根據save選項所設置的保存條件,設置服務器狀態redisServer結構的saveparams屬性

struct redisServer {
    // ...
    // 
記錄了保存條件的數組
    struct saveparam *saveparams;
    // ...
};

struct saveparam {
    // 
秒數
    time_t seconds;
    // 
修改數
    int changes;
};

默認狀況下結構以下:

除了saveparams數組以外,服務器狀態還維持着一個dirty計數器,以及一個lastsave屬性:
·dirty計數器記錄距離上一次成功執行SAVE命令或者BGSAVE命令以後,服務器對數據庫狀態(服務器中的全部數據庫)進行了多少次修改(包括寫入、刪除、更新等操做);
·lastsave屬性是一個UNIX時間戳,記錄了服務器上一次成功執行SAVE命令或者BGSAVE命令的時間。

 Redis的服務器週期性操做函數serverCron默認每隔100毫秒就會執行一次,該函數用於對正在運行的服務器進行維護,它的其中一項工做就是檢查save選項所設置的保存條件是否已經知足,若是知足的話,就執行BGSAVE命令。

如下僞代碼展現了serverCron函數檢查保存條件的過程:

def serverCron():
    # ...
    # 
遍歷全部保存條件
    for saveparam in server.saveparams:
        # 
計算距離上次執行保存操做有多少秒
        save_interval = unixtime_now()-server.lastsave
        # 
若是數據庫狀態的修改次數超過條件所設置的次數,而且距離上次保存的時間超過條件所設置的時間
        # 
那麼執行保存操做
        if      server.dirty >= saveparam.changes and \
           save_interval > saveparam.seconds:
            BGSAVE()
    # ...

RDB文件結構

下圖展現了一個完整RDB文件所包含的各個部分:

爲了方便區分變量、數據、常量,上圖中用全大寫單詞標示常量,用全小寫單詞標示變量和數據

RDB文件的最開頭是REDIS部分,這個部分的長度爲5字節,保存着「REDIS」五個字符。經過這五個字符,程序能夠在載入文件時,快速檢查所載入的文件是否RDB文件。

注意:

由於RDB文件保存的是二進制數據,而不是C字符串,爲了簡便起見,咱們用"REDIS"符號表明'R'、'E'、'D'、'I'、'S'五個字符,而不是帶'\0'結尾符號的C字符串'R'、'E'、'D'、'I'、'S'、'\0'。

db_version長度爲4字節,它的值是一個字符串表示的整數,這個整數記錄了RDB文件的版本號,好比"0006"就表明RDB文件的版本爲第六版。

databases部分包含着零個或任意多個數據庫,以及各個數據庫中的鍵值對數據:·若是服務器的數據庫狀態爲空(全部數據庫都是空的),那麼這個部分也爲空,長度爲0字節。

EOF常量的長度爲1字節,這個常量標誌着RDB文件正文內容的結束,當讀入程序遇到這個值的時候,它知道全部數據庫的全部鍵值對都已經載入完畢了。

check_sum是一個8字節長的無符號整數,保存着一個校驗和。

databases部分

上面提到的databases數據庫結構以下:

SELECTDB常量的長度爲1字節,當讀入程序遇到這個值的時候,它知道接下來要讀入的將是一個數據庫號碼。

db_number保存着一個數據庫號碼,根據號碼的大小不一樣,這個部分的長度能夠是1字節、2字節或者5字節。當程序讀入db_number部分以後,服務器會調用SELECT命令,根據讀入的數據庫號碼進行數據庫切換。

key_value_pairs部分保存了數據庫中的全部鍵值對數據,若是鍵值對帶有過時時間,那麼過時時間也會和鍵值對保存在一塊兒。根據鍵值對的數量、類型、內容以及是否有過時時間等條件的不一樣,key_value_pairs部分的長度也會有所不一樣。

key_value_pairs部分

不帶過時時間的鍵值對在RDB文件中由TYPE、key、value三部分組成,以下圖:

TYPE記錄了value的類型,長度爲1字節,值能夠是如下常量的其中一個:

·REDIS_RDB_TYPE_STRING
·REDIS_RDB_TYPE_LIST
·REDIS_RDB_TYPE_SET
·REDIS_RDB_TYPE_ZSET

·REDIS_RDB_TYPE_HASH

·REDIS_RDB_TYPE_LIST_ZIPLIST
·REDIS_RDB_TYPE_SET_INTSET
·REDIS_RDB_TYPE_ZSET_ZIPLIST
·REDIS_RDB_TYPE_HASH_ZIPLIST

其中key老是一個字符串對象,它的編碼方式和REDIS_RDB_TYPE_STRING類型的value同樣。

帶有過時時間的鍵值對在RDB文件中的結構如圖:

新增的EXPIRETIME_MS和ms,它們的意義以下:
EXPIRETIME_MS常量的長度爲1字節,它告知讀入程序,接下來要讀入的將是一個以毫秒爲單位的過時時間;

·ms是一個8字節長的帶符號整數,記錄着一個以毫秒爲單位的UNIX時間戳,這個時間戳就是鍵值對的過時時間。

關於type的具體講解,請看redis設計與實現書中,RDB文件結構部分。

分析RDB文件

包含字符串鍵的RDB文件(分析一個帶有單個字符串鍵的數據庫)

redis> FLUSHALL
OK
redis> SET MSG "HELLO"
OK
redis> SAVE
OK

執行od命令:

$ od -c dump.rdb
0000000   R   E  D  I  S  0   0   0  6 376  \0 \0 003  M   S  G
0000020 005   H  E  L  L  O 377 207  z  =  304  f   T  L 343
0000037

RDB文件的最開始是REDIS和版本號0006,以後出現的376表明SELECTDB常量,再以後的\0表明整數0,表示被保存的數據庫爲0號數據庫。
在數據庫號碼以後,直到表明EOF常量的377爲止,RDB文件包含有如下內容:

\0 003 M S G 005 H E L L O

\0就是字符串類型的TYPE值REDIS_RDB_TYPE_STRING(這個常量的實際值爲整數0),以後的003是鍵MSG的長度值,再以後的005則是值HELLO的長度。

 包含帶有過時時間的字符串鍵的RDB文件

redis> FLUSHALL
OK
redis> SETEX MSG 10086 "HELLO"
OK
redis> SAVE
OK

$ od -c dump.rdb
0000000   R   E  D   I   S   0   0   0  6 376 \0 374  \  2 365 336
0000020   @ 001 \0  \0  \0 003   M   S  G 005  H   E  L  L   O 377
0000040 212 231  x 247 252   } 021 306
0000050

·一個一字節長的EXPIRETIME_MS特殊值。
·一個八字節長的過時時間(ms)。
·一個一字節長的類型(TYPE)。
·一個鍵(key)和一個值(value)。
根據這些特徵,能夠得出RDB文件各個部分的意義:
·REDIS0006:RDB文件標誌和版本號。
·376\0:切換到0號數據庫。
·374:表明特殊值EXPIRETIME_MS。
·\2 365 336@001\0\0:表明八字節長的過時時間。
·\0 003 M S G:\0表示這是一個字符串鍵,003是鍵的長度,MSG是鍵。
·005 H E L L O:005是值的長度,HELLO是值。
·377:表明EOF常量。

·212 231 x 247 252 } 021 306:表明八字節長的校驗和。

 rdbSave函數具體代碼實現

數據結構 rio

持久化的 IO 操做在 rio.h 和 rio.c 中實現,核心數據結構是 struct rio。RDB 中的幾乎每個函數都帶有 rio 參數。struct rio 既適用於文件,又適用於內存緩存,從 struct rio 的實現可見一斑,它抽象了文件和內存的操做。

/*

 * RIO API 接口和狀態 */
struct _rio { /* 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. */
    // 返回0表示失敗,返回非0表示成功
    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 *); /* 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); /* The current checksum */
    // 當前校驗和
 uint64_t cksum; /* number of bytes read or written */
  //讀或者寫的字節數 size_t processed_bytes; /* maximum single read or write chunk size */
  //最大單次讀 size_t max_processing_chunk; /* Backend-specific vars. */ union { struct { // 緩存指針 sds ptr; // 偏移量 off_t pos; } buffer; struct { // 被打開文件的指針 FILE *fp; // 最近一次 fsync() 以來,寫入的字節量 off_t buffered; /* Bytes written since last fsync. */ // 寫入多少字節以後,纔會自動執行一次 fsync() off_t autosync; /* fsync after 'autosync' bytes written. */ } file; } io; };
typedef struct _rio rio;

 

 redis 定義兩個 struct rio(rio.c中),分別是 rioFileIO 和 rioBufferIO,前者用於內存緩存,後者用於文件 IO:

 

/*
 * 流爲內存時所使用的結構
 */
static const rio rioBufferIO = {
    // 讀函數
  //static size_t rioBufferRead(rio *r, void *buf, size_t len)從 r 中讀取長度爲 len 的內容到 buf 中。 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 */ }; /* * 流爲文件時所使用的結構 */ static const rio rioFileIO = { // 讀函數
  //size_t rioFileRead(rio *r, void *buf, size_t len)從文件 r 中讀取 len 字節到 buf 中。 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 */ };

 

下面查看一下rdbSave源代碼:

/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success 
 * 將數據庫保存到磁盤上。
 * 保存成功返回 REDIS_OK ,出錯/失敗返回 REDIS_ERR 。
 */
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;

    // 建立臨時文件
    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
    rioInitWithFile(&rdb,fp);

    // 設置校驗和函數
    if (server.rdb_checksum)
        rdb.update_cksum = rioGenericUpdateChecksum;

    // 寫入 RDB 版本號
    snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
    if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;//rdbWriteRaw(rio *rdb,void *p,size_t len)將長度爲len的字符數組p寫入到rdb中

    // 遍歷全部數據庫
    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) {
            fclose(fp);
            return REDIS_ERR;
        }

        /* Write the SELECT DB opcode 
         *
         * 寫入數據庫編號
         */
        if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;
        if (rdbSaveLen(&rdb,j) == -1) goto werr;

        /* Iterate this DB writing every entry 
         *
         * 遍歷數據庫,並寫入每一個鍵值對的數據
         */
        while((de = dictNext(di)) != NULL) {
            sds keystr = dictGetKey(de);
            robj key, *o = dictGetVal(de);
            long long expire;
            
            // 根據 keystr ,在棧中建立一個 key 對象
            initStaticStringObject(key,keystr);

            // 獲取鍵的過時時間
            expire = getExpire(db,&key);

            // 保存鍵值對數據
            if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;
        }
        dictReleaseIterator(di);
    }
    di = NULL; /* So that we don't release it again on error. */

    /* EOF opcode 
     *
     * 寫入 EOF 代碼
     */
    if (rdbSaveType(&rdb,REDIS_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. 
     *
     * CRC64 校驗和。
     *
     * 若是校驗和功能已關閉,那麼 rdb.cksum 將爲 0 ,
     * 在這種狀況下, RDB 載入時會跳過校驗和檢查。
     */
    cksum = rdb.cksum;
    memrev64ifbe(&cksum);
    rioWrite(&rdb,&cksum,8);

    /* Make sure data will not remain on the OS's output buffers */
    // 沖洗緩存,確保數據已寫入磁盤
    if (fflush(fp) == EOF) goto werr;
    if (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. 
     *
     * 使用 RENAME ,原子性地對臨時文件進行更名,覆蓋原來的 RDB 文件。
     */
    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;

    // 記錄最後一次完成 SAVE 的時間
    server.lastsave = time(NULL);

    // 記錄最後一次執行 SAVE 的狀態
    server.lastbgsave_status = REDIS_OK;

    return REDIS_OK;

werr:
    // 關閉文件
    fclose(fp);
    // 刪除文件
    unlink(tmpfile);

    redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));

    if (di) dictReleaseIterator(di);

    return REDIS_ERR;
}

 rdbLoad函數源碼

int rdbLoad(char *filename) {
    uint32_t dbid;
    int type, rdbver;
    redisDb *db = server.db+0;
    char buf[1024];
    long long expiretime, now = mstime();
    FILE *fp;
    rio rdb;

    // 打開 rdb 文件
    if ((fp = fopen(filename,"r")) == NULL) return REDIS_ERR;

    // 初始化寫入流
    rioInitWithFile(&rdb,fp);
    rdb.update_cksum = rdbLoadProgressCallback;// 記錄載入進度信息,以便讓客戶端進行查詢,這也會在計算 RDB 校驗和時用到。
    rdb.max_processing_chunk = server.loading_process_events_interval_bytes;
    if (rioRead(&rdb,buf,9) == 0) goto eoferr;
    buf[9] = '\0';

    //取出最前面的REDIS字符,若是不是REDIS字符,那麼就不是rdb文件
    if (memcmp(buf,"REDIS",5) != 0) {
        fclose(fp);
        redisLog(REDIS_WARNING,"Wrong signature trying to load DB from file");
        errno = EINVAL;
        return REDIS_ERR;
    }
    // 檢查版本號
    rdbver = atoi(buf+5);
    if (rdbver < 1 || rdbver > REDIS_RDB_VERSION) {
        fclose(fp);
        redisLog(REDIS_WARNING,"Can't handle RDB format version %d",rdbver);
        errno = EINVAL;
        return REDIS_ERR;
    }

    // 將服務器狀態調整到開始載入狀態
    startLoading(fp);
    while(1) {
        robj *key, *val;
        expiretime = -1;

        /* Read type. 
         * 讀入類型指示,決定該如何讀入以後跟着的數據。
         * 這個指示能夠是 rdb.h 中定義的全部以
         * REDIS_RDB_TYPE_* 爲前綴的常量的其中一個
         * 或者全部以 REDIS_RDB_OPCODE_* 爲前綴的常量的其中一個
         */
        if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;

        // 讀入過時時間值
        if (type == REDIS_RDB_OPCODE_EXPIRETIME) {

            // 以秒計算的過時時間

            if ((expiretime = rdbLoadTime(&rdb)) == -1) goto eoferr;

            /* We read the time so we need to read the object type again. 
             *
             * 在過時時間以後會跟着一個鍵值對,咱們要讀入這個鍵值對的類型
             */
            if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;

            /* the EXPIRETIME opcode specifies time in seconds, so convert
             * into milliseconds. 
             *
             * 將格式轉換爲毫秒*/
            expiretime *= 1000;
        } else if (type == REDIS_RDB_OPCODE_EXPIRETIME_MS) {

            // 以毫秒計算的過時時間

            /* Milliseconds precision expire times introduced with RDB
             * version 3. */
            if ((expiretime = rdbLoadMillisecondTime(&rdb)) == -1) goto eoferr;

            /* We read the time so we need to read the object type again.
             *
             * 在過時時間以後會跟着一個鍵值對,咱們要讀入這個鍵值對的類型
             */
            if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
        }
            
        // 讀入數據 EOF (不是 rdb 文件的 EOF)
        if (type == REDIS_RDB_OPCODE_EOF)
            break;

        /* Handle SELECT DB opcode as a special case 
         *
         * 若是讀入的是REDIS_RDB_OPCODE_SELECTDB,那麼切換數據庫
         */
        if (type == REDIS_RDB_OPCODE_SELECTDB) {

            // 讀入數據庫號碼
            if ((dbid = rdbLoadLen(&rdb,NULL)) == REDIS_RDB_LENERR)
                goto eoferr;

            // 檢查數據庫號碼的正確性
            if (dbid >= (unsigned)server.dbnum) {
                redisLog(REDIS_WARNING,"FATAL: Data file was created with a Redis server configured to handle more than %d databases. Exiting\n", server.dbnum);
                exit(1);
            }

            // 在程序內容切換數據庫
            db = server.db+dbid;
            continue;
        }

        /* Read key 
         *
         * 讀入鍵
         */
        if ((key = rdbLoadStringObject(&rdb)) == NULL) goto eoferr;

        /* Read value 
         *
         * 讀入值
         */
        if ((val = rdbLoadObject(type,&rdb)) == NULL) goto eoferr;

        /* Check if the key already expired. This function is used when loading
         * an RDB file from disk, either at startup, or when an RDB was
         * received from the master. In the latter case, the master is
         * responsible for key expiry. If we would expire keys here, the
         * snapshot taken by the master may not be reflected on the slave. 
         *
         * 若是服務器不是主節點,
         * 那麼在鍵已通過期的時候,再也不將它們關聯到數據庫中去
         */
        if (server.masterhost == NULL && expiretime != -1 && expiretime < now) {
            decrRefCount(key);
            decrRefCount(val);
            // 跳過
            continue;
        }

        /* Add the new object in the hash table 
         *
         * 將鍵值對關聯到數據庫中
         */
        dbAdd(db,key,val);

        /* Set the expire time if needed 
         *
         * 設置過時時間
         */
        if (expiretime != -1) setExpire(db,key,expiretime);

        decrRefCount(key);//爲對象的引用計數減一???
    }

    /* Verify the checksum if RDB version is >= 5 
     *
     * 若是 RDB 版本 >= 5 ,那麼比對校驗和
     */
    if (rdbver >= 5 && server.rdb_checksum) {
        uint64_t cksum, expected = rdb.cksum;

        // 讀入文件的校驗和
        if (rioRead(&rdb,&cksum,8) == 0) goto eoferr;
        memrev64ifbe(&cksum);

        // 比對校驗和
        if (cksum == 0) {
            redisLog(REDIS_WARNING,"RDB file was saved with checksum disabled: no check performed.");
        } else if (cksum != expected) {
            redisLog(REDIS_WARNING,"Wrong RDB checksum. Aborting now.");
            exit(1);
        }
    }

    // 關閉 RDB 
    fclose(fp);

    // 服務器從載入狀態中退出
    stopLoading();

    return REDIS_OK;

eoferr: /* unexpected end of file is handled here with a fatal exit */
    redisLog(REDIS_WARNING,"Short read or OOM loading DB. Unrecoverable error, aborting now.");
    exit(1);
    return REDIS_ERR; /* Just to avoid warning */
}

 AOF持久化

AOF持久化是經過保存Redis服務器所執行的寫命令來記錄數據庫狀態的。

如:

redis> SET msg "hello"
OK
redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> RPUSH numbers 128 256 512
(integer) 3

被寫入AOF文件的全部命令都是以Redis的命令請求協議格式保存的,由於Redis的命令請求協議是純文本格式,因此咱們能夠直接打開一個AOF文件。

*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n
*5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\n
*5\r\n$5\r\nRPUSH\r\n$7\r\nnumbers\r\n$3\r\n128\r\n$3\r\n256\r\n$3\r\n512\r\n

在這個AOF文件裏面,除了用於指定數據庫的SELECT命令是服務器自動添加的以外,其餘都是咱們以前經過客戶端發送的命令。

AOF持久化的實現

AOF持久化功能的實現能夠分爲命令追加(append)、文件寫入、文件同步(sync)三個步驟。

命令追加

當AOF持久化功能處於打開狀態時,服務器在執行完一個寫命令以後,會以協議格式將被執行的寫命令追加到服務器狀態的aof_buf緩衝區的末尾

struct redisServer {
    // ...
    // AOF
緩衝區
    sds aof_buf;
    // ...
};

如:若是客戶端向服務器發送如下命令:

redis> SET KEY VALUE
OK

那麼服務器在執行這個SET命令以後,會將如下協議內容追加到aof_buf緩衝區的末尾:

*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n

AOF文件的寫入與同步

由於服務器在處理文件事件時可能會執行寫命令,使得一些內容被追加到aof_buf緩衝區裏面,因此在服務器每次結束一個事件循環以前,它都會調用flushAppendOnlyFile函數,考慮是否須要將aof_buf緩衝區中的內容寫入和保存到AOF文件裏面。

flushAppendOnlyFile函數的行爲由服務器配置的appendfsync選項的值來決定,各個不一樣值產生的行爲以下表所示:

若是用戶沒有主動爲appendfsync選項設置值,那麼appendfsync選項的默認值爲everysec。

AOF文件的載入與數據還原

由於AOF文件裏面包含了重建數據庫狀態所需的全部寫命令,因此服務器只要讀入並從新執行一遍AOF文件裏面保存的寫命令,就能夠還原服務器關閉以前的數據庫狀態。

Redis讀取AOF文件並還原數據庫狀態的詳細步驟以下:

建立一個不帶網絡鏈接的僞客戶端(fake client):由於Redis的命令只能在客戶端上下文中執行,而載入AOF文件時所使用的命令直接來源於AOF文件而不是網絡鏈接,因此服務器使用了一個沒有網絡鏈接的僞客戶端來執行AOF文件保存的寫命令,僞客戶端執行命令的效果和帶網絡鏈接的客戶端執行命令的效果徹底同樣。

AOF重寫

重寫是爲了解決AOF文件愈來愈大的問題,因此須要將他的體積縮小。

AOF文件重寫的實現

首先從數據庫中讀取鍵如今的值,而後用一條命令去記錄鍵值對,代替以前記錄這個鍵值對的多條命令,這就是AOF重寫功能的實現原理。

如:

redis> SADD animals "Cat"            
   // {"Cat"}
(integer) 1
redis> SADD animals "Dog" "Panda" "Tiger"    // {"Cat", "Dog", "Panda", "Tiger"}
(integer) 3
redis> SREM animals "Cat"                    // {"Dog", "Panda", "Tiger"}
(integer) 1
redis> SADD animals "Lion" "Cat"             // {"Dog", "Panda", "Tiger", 
(integer) 2                                     "Lion", "Cat"}

能夠用:

SADD animals"Dog""Panda""Tiger""Lion""Cat" 命令代替。

注意:

在實際中,爲了不在執行命令時形成客戶端輸入緩衝區溢出,重寫程序在處理列表、哈希表、集合、有序集合這四種可能會帶有多個元素的鍵時,會先檢查鍵所包含的元素數量,若是元素的數量超過了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那麼重寫程序將使用多條命令來記錄鍵的值,而不僅僅使用一條命令。
在目前版本中,REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值爲64,這也就是說,若是一個集合鍵包含了超過64個元素,那麼重寫程序會用多條SADD命令來記錄這個集合,而且每條命令設置的元素數量也爲64個:

SADD <set-key> <elem1> <elem2> ... <elem64>
SADD <set-key> <elem65> <elem66> ... <elem128>
SADD <set-key> <elem129> <elem130> ... <elem192>
...

aof的重寫是也是放在子程序中進行。不過,使用子進程也有一個問題須要解決,由於子進程在進行AOF重寫期間,服務器進程還須要繼續處理命令請求,而新的命令可能會對現有的數據庫狀態進行修改,從而使得服務器當前的數據庫狀態和重寫後的AOF文件所保存的數據庫狀態不一致。

爲了解決這種數據不一致問題,Redis服務器設置了一個AOF重寫緩衝區,這個緩衝區在服務器建立子進程以後開始使用,當Redis服務器執行完一個寫命令以後,它會同時將這個寫命令發送給AOF緩衝區和AOF重寫緩衝區,如圖所示:

這也就是說,在子進程執行AOF重寫期間,服務器進程須要執行如下三個工做:
1)執行客戶端發來的命令;
2)將執行後的寫命令追加到AOF緩衝區;
3)將執行後的寫命令追加到AOF重寫緩衝區。
這樣一來能夠保證:
·AOF緩衝區的內容會按期被寫入和同步到AOF文件,對現有AOF文件的處理工做會如常進行。
·從建立子進程開始,服務器執行的全部寫命令都會被記錄到AOF重寫緩衝區裏面。

當子進程完成AOF重寫工做以後,它會向父進程發送一個信號,父進程在接到該信號以後,會調用一個信號處理函數,並執行如下工做:

1)將AOF重寫緩衝區中的全部內容寫入到新AOF文件中,這時新AOF文件所保存的數據庫狀態將和服務器當前的數據庫狀態一致。
2)對新的AOF文件進行更名,原子地(atomic)覆蓋現有的AOF文件,完成新舊兩個AOF文件的替換。

這個信號處理函數執行完畢以後,父進程就能夠繼續像往常同樣接受命令請求了。

注意:在整個AOF後臺重寫過程當中,只有信號處理函數執行時會對服務器進程(父進程)形成阻塞,在其餘時候,AOF後臺重寫都不會阻塞父進程,這將AOF重寫對服務器性能形成的影響降到了最低。

具體函數源碼分析,能夠參考:http://wiki.jikexueyuan.com/project/redis/aof.html(感受代碼的註釋並非很好,看英文註釋比較靠譜)。

相關文章
相關標籤/搜索