redis 數(shù)據恢復過程

系列

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

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

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

  • aof 數(shù)據文件
  • rdb 數(shù)據文件
    數(shù)據恢復的過程是二選一的過程分扎,也就是如果開啟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ù)據恢復過程

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

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

?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);
    }

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

    fakeClient = createFakeClient();

    // 設置服務器的狀態(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();
        }

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

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

        // 至少要有一個參數(shù)(被調用的命令)
        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)建三個包含以下內容的字符串對象:
        // 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);
        }

        /* 
         * 調用偽客戶端彼城,執(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 文件的全部內容都可以正確地讀取,
     * 但是募壕,還要檢查 AOF 是否包含未正確結束的事務
     */
    if (fakeClient->flags & REDIS_MULTI) goto readerr;

    // 關閉 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");
    
    // 文件內容出錯
    } else {
        redisLog(REDIS_WARNING,"Unrecoverable error reading the append only file: %s", strerror(errno));
    }
    exit(1);

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


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

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

整體恢復步驟如下:

  • 打開rdb文件開始恢復數(shù)據
  • 讀取type用于判斷讀取value的格式
  • 讀取key且key的第一個字節(jié)標明了key的長度所以可以讀取準確長度的key
  • 讀取value對象,讀取過程根據type進行讀取以及恢復
/*
 * 將給定 rdb 中保存的數(shù)據載入到數(shù)據庫中。
 */
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)調整到開始載入狀態(tài)
    startLoading(fp);
    while(1) {
        robj *key, *val;
        expiretime = -1;

        /* Read type. 
         *
         * 讀入類型指示棘钞,決定該如何讀入之后跟著的數(shù)據缠借。
         *
         * 這個指示可以是 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;
        }
            
        // 讀入數(shù)據 EOF (不是 rdb 文件的 EOF)
        if (type == REDIS_RDB_OPCODE_EOF)
            break;

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

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

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

            // 在程序內容切換數(shù)據庫
            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é)點的話泼返,
         * 那么在鍵已經過期的時候,不再將它們關聯(lián)到數(shù)據庫中去
         */
        if (server.masterhost == NULL && expiretime != -1 && expiretime < now) {
            decrRefCount(key);
            decrRefCount(val);
            // 跳過
            continue;
        }

        /* Add the new object in the hash table 
         *
         * 將鍵值對關聯(lián)到數(shù)據庫中
         */
        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);

    // 服務器從載入狀態(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 */
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末绅喉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子叫乌,更是在濱河造成了極大的恐慌柴罐,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件憨奸,死亡現(xiàn)場離奇詭異革屠,居然都是意外死亡,警方通過查閱死者的電腦和手機膀藐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門屠阻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人额各,你說我怎么就攤上這事“墒眩” “怎么了虾啦?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長痕寓。 經常有香客問我傲醉,道長,這世上最難降的妖魔是什么呻率? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任硬毕,我火速辦了婚禮,結果婚禮上礼仗,老公的妹妹穿的比我還像新娘吐咳。我一直安慰自己,他們只是感情好元践,可當我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布韭脊。 她就那樣靜靜地躺著,像睡著了一般单旁。 火紅的嫁衣襯著肌膚如雪沪羔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天象浑,我揣著相機與錄音蔫饰,去河邊找鬼琅豆。 笑死,一個胖子當著我的面吹牛篓吁,可吹牛的內容都是我干的趋距。 我是一名探鬼主播,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼越除,長吁一口氣:“原來是場噩夢啊……” “哼节腐!你這毒婦竟也來了?” 一聲冷哼從身側響起摘盆,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤翼雀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后孩擂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狼渊,經...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年类垦,在試婚紗的時候發(fā)現(xiàn)自己被綠了狈邑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蚤认,死狀恐怖米苹,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情砰琢,我是刑警寧澤蘸嘶,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站陪汽,受9級特大地震影響训唱,放射性物質發(fā)生泄漏。R本人自食惡果不足惜挚冤,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一况增、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧训挡,春花似錦澳骤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至表悬,卻和暖如春弥锄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工籽暇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留温治,地道東北人。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓戒悠,卻偏偏與公主長得像熬荆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子绸狐,可洞房花燭夜當晚...
    茶點故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內容

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