redis 數(shù)據(jù)恢復過程

系列

redis數(shù)據(jù)淘汰原理
redis過期數(shù)據(jù)刪除策略
redis server事件模型
redis cluster mget 引發(fā)的討論
redis 3.x windows 集群搭建
redis 命令執(zhí)行過程
redis string底層數(shù)據(jù)結(jié)構(gòu)
redis list底層數(shù)據(jù)結(jié)構(gòu)
redis hash底層數(shù)據(jù)結(jié)構(gòu)
redis set底層數(shù)據(jù)結(jié)構(gòu)
redis zset底層數(shù)據(jù)結(jié)構(gòu)
redis 客戶端管理
redis 主從同步-slave端
redis 主從同步-master端
redis 主從超時檢測
redis aof持久化
redis rdb持久化
redis 數(shù)據(jù)恢復過程
redis TTL實現(xiàn)原理
redis cluster集群建立
redis cluster集群選主

redis的數(shù)據(jù)恢復過程

?redis的數(shù)據(jù)載入主要是指redis重啟時候恢復數(shù)據(jù)的過程,恢復的數(shù)據(jù)總共有兩種:

  • aof 數(shù)據(jù)文件
  • rdb 數(shù)據(jù)文件
    數(shù)據(jù)恢復的過程是二選一的過程,也就是如果開啟aof持久化那么就會使用aof文件進行恢復,如果沒有才會選擇rdb文件進行恢復。
void loadDataFromDisk(void) {
    // 記錄開始時間
    long long start = ustime();

    // AOF 持久化已打開?
    if (server.aof_state == REDIS_AOF_ON) {
        // 嘗試載入 AOF 文件
        if (loadAppendOnlyFile(server.aof_filename) == REDIS_OK)
            // 打印載入信息,并計算載入耗時長度
            redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    // AOF 持久化未打開
    } else {
        // 嘗試載入 RDB 文件
        if (rdbLoad(server.rdb_filename) == REDIS_OK) {
            // 打印載入信息,并計算載入耗時長度
            redisLog(REDIS_NOTICE,"DB loaded from disk: %.3f seconds",
                (float)(ustime()-start)/1000000);
        } else if (errno != ENOENT) {
            redisLog(REDIS_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno));
            exit(1);
        }
    }
}


redis aof數(shù)據(jù)恢復過程

?整個aof文件載入的過程其實是非常簡單,整體步驟如下:

  • 打開aof文件開始循環(huán)讀取
  • 根據(jù)aof寫入的命令解析redis 命令行
  • 通過偽命令行客戶端執(zhí)行解析的命令行
  • redis接收到偽客戶端發(fā)送的命令行以后找到命令對應的函數(shù)負責執(zhí)行數(shù)據(jù)寫入

?aof保存的命令行格式類似"*3\r\n3\r\nSET\r\n5\r\nmykey\r\n7\r\nmyvalue\r\n", 所以解析到\*字符就知道是一個命令的開始,然后就知道命令涉及的參數(shù)個數(shù),每個參數(shù)都以字符開始標記字符串長度,知道字符串長度就可以解析出命令字符串了。

int loadAppendOnlyFile(char *filename) {

    // 為客戶端
    struct redisClient *fakeClient;

    // 打開 AOF 文件
    FILE *fp = fopen(filename,"r");

    struct redis_stat sb;
    int old_aof_state = server.aof_state;
    long loops = 0;

    // 檢查文件的正確性
    if (fp && redis_fstat(fileno(fp),&sb) != -1 && sb.st_size == 0) {
        server.aof_current_size = 0;
        fclose(fp);
        return REDIS_ERR;
    }

    // 檢查文件是否正常打開
    if (fp == NULL) {
        redisLog(REDIS_WARNING,"Fatal error: can't open the append log file for reading: %s",strerror(errno));
        exit(1);
    }

    /* 
     *
     * 暫時性地關(guān)閉 AOF ,防止在執(zhí)行 MULTI 時,
     * EXEC 命令被傳播到正在打開的 AOF 文件中。
     */
    server.aof_state = REDIS_AOF_OFF;

    fakeClient = createFakeClient();

    // 設(shè)置服務器的狀態(tài)為:正在載入
    // startLoading 定義于 rdb.c
    startLoading(fp);

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

        /* 
         * 間隔性地處理客戶端發(fā)送來的請求
         * 因為服務器正處于載入狀態(tài),所以能正常執(zhí)行的只有 PUBSUB 等模塊
         */
        if (!(loops++ % 1000)) {
            loadingProgress(ftello(fp));
            processEventsWhileBlocked();
        }

        // 讀入文件內(nèi)容到緩存
        if (fgets(buf,sizeof(buf),fp) == NULL) {
            if (feof(fp))
                // 文件已經(jīng)讀完,跳出
                break;
            else
                goto readerr;
        }

        // 確認協(xié)議格式,比如 *3\r\n
        if (buf[0] != '*') goto fmterr;
        
        // 取出命令參數(shù),比如 *3\r\n 中的 3
        argc = atoi(buf+1);

        // 至少要有一個參數(shù)(被調(diào)用的命令)
        if (argc < 1) goto fmterr;

        // 從文本中創(chuàng)建字符串對象:包括命令,以及命令參數(shù)
        // 例如 $3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
        // 將創(chuàng)建三個包含以下內(nèi)容的字符串對象:
        // SET 、 KEY 、 VALUE
        argv = zmalloc(sizeof(robj*)*argc);
        for (j = 0; j < argc; j++) {
            if (fgets(buf,sizeof(buf),fp) == NULL) goto readerr;

            if (buf[0] != '$') goto fmterr;

            // 讀取參數(shù)值的長度
            len = strtol(buf+1,NULL,10);
            // 讀取參數(shù)值
            argsds = sdsnewlen(NULL,len);
            if (len && fread(argsds,len,1,fp) == 0) goto fmterr;
            // 為參數(shù)創(chuàng)建對象
            argv[j] = createObject(REDIS_STRING,argsds);

            if (fread(buf,2,1,fp) == 0) goto fmterr; /* discard CRLF */
        }

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

        /* 
         * 調(diào)用偽客戶端,執(zhí)行命令
         */
        fakeClient->argc = argc;
        fakeClient->argv = argv;
        cmd->proc(fakeClient);

        /* The fake client should not have a reply */
        redisAssert(fakeClient->bufpos == 0 && listLength(fakeClient->reply) == 0);
        /* The fake client should never get blocked */
        redisAssert((fakeClient->flags & REDIS_BLOCKED) == 0);

        /*
         * 清理命令和命令參數(shù)對象
         */
        for (j = 0; j < fakeClient->argc; j++)
            decrRefCount(fakeClient->argv[j]);
        zfree(fakeClient->argv);
    }

    /* 
     * 如果能執(zhí)行到這里,說明 AOF 文件的全部內(nèi)容都可以正確地讀取,
     * 但是,還要檢查 AOF 是否包含未正確結(jié)束的事務
     */
    if (fakeClient->flags & REDIS_MULTI) goto readerr;

    // 關(guān)閉 AOF 文件
    fclose(fp);
    // 釋放偽客戶端
    freeFakeClient(fakeClient);
    // 復原 AOF 狀態(tài)
    server.aof_state = old_aof_state;
    // 停止載入
    stopLoading();
    // 更新服務器狀態(tài)中, AOF 文件的當前大小
    aofUpdateCurrentSize();
    // 記錄前一次重寫時的大小
    server.aof_rewrite_base_size = server.aof_current_size;
    
    return REDIS_OK;

// 讀入錯誤
readerr:
    // 非預期的末尾,可能是 AOF 文件在寫入的中途遭遇了停機
    if (feof(fp)) {
        redisLog(REDIS_WARNING,"Unexpected end of file reading the append only file");
    
    // 文件內(nèi)容出錯
    } else {
        redisLog(REDIS_WARNING,"Unrecoverable error reading the append only file: %s", strerror(errno));
    }
    exit(1);

// 內(nèi)容格式錯誤
fmterr:
    redisLog(REDIS_WARNING");
    exit(1);
}


redis rdb數(shù)據(jù)恢復過程

?整個rdb文件載入的過程其實是非常簡單,不過和aof有些許差別:
rdb文件的數(shù)據(jù)恢復直接寫入內(nèi)存而不是通過偽裝命令行執(zhí)行命令生成的
rdb文件的讀取過程和aof不一樣,rdb文件存儲按照type+key+value的格式存儲所以讀取也是這樣讀取的

整體恢復步驟如下:

  • 打開rdb文件開始恢復數(shù)據(jù)
  • 讀取type用于判斷讀取value的格式
  • 讀取key且key的第一個字節(jié)標明了key的長度所以可以讀取準確長度的key
  • 讀取value對象,讀取過程根據(jù)type進行讀取以及恢復
/*
 * 將給定 rdb 中保存的數(shù)據(jù)載入到數(shù)據(jù)庫中。
 */
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.max_processing_chunk = server.loading_process_events_interval_bytes;
    if (rioRead(&rdb,buf,9) == 0) goto eoferr;
    buf[9] = '\0';

    // 檢查版本號
    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;
    }

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

        /* Read type. 
         *
         * 讀入類型指示,決定該如何讀入之后跟著的數(shù)據(jù)。
         *
         * 這個指示可以是 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. 
             *
             * 將格式轉(zhuǎn)換為毫秒*/
            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;
        }
            
        // 讀入數(shù)據(jù) EOF (不是 rdb 文件的 EOF)
        if (type == REDIS_RDB_OPCODE_EOF)
            break;

        /* 
         * 讀入切換數(shù)據(jù)庫指示
         */
        if (type == REDIS_RDB_OPCODE_SELECTDB) {

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

            // 檢查數(shù)據(jù)庫號碼的正確性
            if (dbid >= (unsigned)server.dbnum) {
                redisLog(REDIS_WARNING,"FATAL: ", server.dbnum);
                exit(1);
            }

            // 在程序內(nèi)容切換數(shù)據(jù)庫
            db = server.db+dbid;

            // 跳過
            continue;
        }

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

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

        /* 
         *
         * 如果服務器為主節(jié)點的話,
         * 那么在鍵已經(jīng)過期的時候,不再將它們關(guān)聯(lián)到數(shù)據(jù)庫中去
         */
        if (server.masterhost == NULL && expiretime != -1 && expiretime < now) {
            decrRefCount(key);
            decrRefCount(val);
            // 跳過
            continue;
        }

        /* Add the new object in the hash table 
         *
         * 將鍵值對關(guān)聯(lián)到數(shù)據(jù)庫中
         */
        dbAdd(db,key,val);

        /* Set the expire time if needed 
         *
         * 設(shè)置過期時間
         */
        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);
        }
    }

    // 關(guān)閉 RDB 
    fclose(fp);

    // 服務器從載入狀態(tài)中退出
    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 */
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 在上一篇文章中,介紹了Redis內(nèi)存模型,從這篇文章開始,將依次介紹Redis高可用相關(guān)的知識——持久化、復...
    Java架構(gòu)閱讀 2,500評論 3 21
  • 文章已經(jīng)放到github上 ,如果對您有幫助 請給個star[https://github.com/qqxuanl...
    尼爾君閱讀 2,329評論 0 22
  • 超強、超詳細Redis入門教程 轉(zhuǎn)載2017年03月04日 16:20:02 16916 轉(zhuǎn)載自: http://...
    邵云濤閱讀 17,626評論 3 313
  • 亞亞經(jīng)常跟著她的伙伴們到山上去玩。那座山不高,遠處看去一片青翠,但不知為何,遍地是朱紅色的泥土。亞亞卻很喜歡,每次...
    chajn閱讀 700評論 2 6
  • 0204-安好-【動待花開】- 7期踐行-20171014-D4 白天坐電梯出門,我家27樓,電梯往下下的時候,孩...
    如此1314安好閱讀 438評論 0 50

友情鏈接更多精彩內(nèi)容