redis源碼分析(7)——rdb

來源:互聯網
上載者:User

標籤:redis   c   

RDB是redis的另一種持久化方式,相當於是定時快照,也用於主從同步中快照+redo log。redis在進行RDB時,不需要加鎖,這是通過利用父子進程共用同一份記憶體完成的。在父進程fork子進程之後,父子以copy-on-write方式共用同一份實體記憶體,當兩個進程寫記憶體時,才會按照記憶體頁複製記憶體。這就需要保證在RDB時,最壞情況下需要保證有2倍的記憶體空間用於父子進程使用(redis使用時佔用1G,那麼就要保證系統有2G的記憶體,否則可能會出現使用swap的情況)。因為copy-on-write,所以需要避免不必要的記憶體拷貝。子進程中基本上只需要讀記憶體,而父進程響應用戶端請求,就需要修改記憶體,為了減少記憶體修改,父進程會暫停keyspace對應的hash表的rehash(rehash會有大量拷貝,要在不同的桶之間拷貝資料)。下面看一下RDB相關內容。

1. RDB檔案格式

RDB檔案格式比較簡單,可以看做是一條條指令序列,每條指令的組成:

|-----------------------|----------------------------------|

| OP code: 1Byte  |       Instruction: nBytes    |

|-----------------------|----------------------------------|

在載入RDB時,就是對這個指令序列進行解析。所有的OP code包括:

REDIS_RDB_OPCODE_EXPIRETIME_MS: ms級的到期時間

REDIS_RDB_OPCODE_EXPIRETIME:秒級的到期時間

REDIS_RDB_OPCODE_SELECTDB:用於select db命令

REDIS_RDB_OPCODE_EOF:RDB檔案結尾

OP code還包括所有的資料類型(REDIS_RDB_TYPE_LIST, REDIS_RDB_TYPE_SET等),用於指定後續kv對中,value的類型。

RDB檔案的前5個位元組是magic number,用於表示檔案是RDB檔案。接下來的4個位元組是版本號碼,在載入RDB時,會根據RDB的版本號碼和redis的版本號碼比較,查看是否可以處理該版本的RDB。然後,是一條條指令序列,最後以EOF結尾。

2. RDB dump

首先看一下dump的時機,主要分為3塊:

1)save命令:用戶端發送save命令,redis執行個體阻塞執行dump。在saveCommand函數中。

2)bgsave命令:dump任務由子進程完成,主進程可以繼續服務要求。在bgsaveCommand函數中。

3)被動觸發:redis變更次數或者dump的間隔超過閾值。在serverCron中,檢測並觸發。

4)主從同步觸發:在不能實現partial sync時,master需要將rdb傳輸給slave。在syncCommand函數中。

下面看一下rdb dump的具體過程,這是由rdbSave函數完成的。

    // <MM>    // 建立並開啟臨時rdb檔案    // </MM>    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;    }    rioInitWithFile(&rdb,fp);    if (server.rdb_checksum)        rdb.update_cksum = rioGenericUpdateChecksum;
建立並開啟臨時檔案,這是為了保證rdb的資料完整性,只有在dump成功後,才會替換原檔案。然後是初始化rio,用於輸出。

    // <MM>    // 寫入magic number,format:    // 9bit: REDIS[RDB_VERSION]    // </MM>    snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);    if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
寫入magic number以及版本號碼。

接下來是一個迴圈,用於對每個redis DB遍曆,並產生對應的內容。

    for (j = 0; j < server.dbnum; j++) {        // dump該DB    }
看下每個DB的dump過程,實際上就是遍曆並輸出每個Key-Value對。

        redisDb *db = server.db+j;        dict *d = db->dict;        if (dictSize(d) == 0) continue;        di = dictGetSafeIterator(d);        if (!di) {            fclose(fp);            return REDIS_ERR;        }
先判斷DB是否為空白,如果為空白就跳過。然後擷取DB的迭代器。

        /* Write the SELECT DB opcode */        if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;        if (rdbSaveLen(&rdb,j) == -1) goto werr;
輸出select db的opcode,然後是對應的DB號。具體格式是,1位元組的OPcode,加上1,2或4位元組的DB號。

        /* Iterate this DB writing every entry */        while((de = dictNext(di)) != NULL) {            sds keystr = dictGetKey(de);            robj key, *o = dictGetVal(de);            long long expire;            initStaticStringObject(key,keystr);            expire = getExpire(db,&key);            if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;        }        dictReleaseIterator(di);
然後是一個while迴圈,遍曆所有K-V對,並進行dump。對於每個KV,擷取key,value和expire time。然後調用rdbSaveKeyValuePair函數進行dump,下面就看一下這個函數。

/* Save a key-value pair, with expire time, type, key, value. * On error -1 is returned. * On success if the key was actually saved 1 is returned, otherwise 0 * is returned (the key was already expired). */int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,                        long long expiretime, long long now){    /* Save the expire time */    if (expiretime != -1) {        /* If this key is already expired skip it */        if (expiretime < now) return 0;        if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;        if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;    }    /* Save type, key, value */    if (rdbSaveObjectType(rdb,val) == -1) return -1;    if (rdbSaveStringObject(rdb,key) == -1) return -1;    if (rdbSaveObject(rdb,val) == -1) return -1;    return 1;}
如果expire不為空白,則輸出該資訊。先寫出OPCode表示expire,然後是具體的逾時時間。接下來是具體的KV對,首先也類似OPCode,表示value的類型,然後是字串類型的key,最後是value對象。具體對象的dump內容比較多,這裡暫時不展開。

上面完成所有DB的dump後,接下來看一下收尾工作。

    di = NULL; /* So that we don‘t release it again on error. */    /* EOF opcode */    if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
輸出EOF對應的OPCode,表示rdb結束。

    /* CRC64 checksum. It will be zero if checksum computation is disabled, the     * loading code skips the check in this case. */    cksum = rdb.cksum;    memrev64ifbe(&cksum);    if (rioWrite(&rdb,&cksum,8) == 0) goto werr;
計算並輸出CRC。

    /* 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;
首先是調用fflush將輸出緩衝區重新整理到page cache,然後調用fsync將cache中的內容寫盤,最後關閉檔案。

    /* Use RENAME to make sure the DB file is changed atomically only     * if the generate DB file is ok. */    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;    server.lastsave = time(NULL);    server.lastbgsave_status = REDIS_OK;    return REDIS_OK;
最後,列印日誌,重設dirty和lastsave,這兩個值會影響被動觸發rdb dump的時機。

werr:    fclose(fp);    unlink(tmpfile);    redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));    if (di) dictReleaseIterator(di);    return REDIS_ERR;
上述出錯的錯誤處理,主要就是刪除臨時檔案,銷毀迭代器並列印日誌。

上面就是整個rdb dump的過程,在後台進行rdb dump時,上述是在子進程中完成的,主進程還需要進行最後的一些清理工作,下面看一下這個部分。在serverCron中,如果server.rdb_child_pid不為-1(存在rdb dump的子進程),會調用wait3對子進程收割,如果是rdb子進程完成,會調用backgroundSaveDoneHandler函數做最後處理。

/* A background saving child (BGSAVE) terminated its work. Handle this. */void backgroundSaveDoneHandler(int exitcode, int bysignal) {    if (!bysignal && exitcode == 0) {        redisLog(REDIS_NOTICE,            "Background saving terminated with success");        server.dirty = server.dirty - server.dirty_before_bgsave;        server.lastsave = time(NULL);        server.lastbgsave_status = REDIS_OK;    } else if (!bysignal && exitcode != 0) {        redisLog(REDIS_WARNING, "Background saving error");        server.lastbgsave_status = REDIS_ERR;    } else {        mstime_t latency;        redisLog(REDIS_WARNING,            "Background saving terminated by signal %d", bysignal);        latencyStartMonitor(latency);        rdbRemoveTempFile(server.rdb_child_pid);        latencyEndMonitor(latency);        latencyAddSampleIfNeeded("rdb-unlink-temp-file",latency);        /* SIGUSR1 is whitelisted, so we have a way to kill a child without         * tirggering an error conditon. */        if (bysignal != SIGUSR1)            server.lastbgsave_status = REDIS_ERR;    }    server.rdb_child_pid = -1;    server.rdb_save_time_last = time(NULL)-server.rdb_save_time_start;    server.rdb_save_time_start = -1;    /* Possibly there are slaves waiting for a BGSAVE in order to be served     * (the first stage of SYNC is a bulk transfer of dump.rdb) */    updateSlavesWaitingBgsave((!bysignal && exitcode == 0) ? REDIS_OK : REDIS_ERR);}
相對於aof rewrite,這塊工作要簡單一些,主要是根據子進程的退出狀態以及是否被訊號kill進行處理。最後一個函數updateSlavesWaitingBgsave是用於在主從同步中,完成rdb dump,通知向slave傳輸rdb。

3. RDB Load

RDB載入主要是兩個地方會用到:

1)redis啟動時,載入RDB

2)主從同步時,master向從發送RDB

redis啟動時載入RDB,和AOF一樣,是在loadDataFromDisk函數:

/* Function called at startup to load RDB or AOF file in memory. */void loadDataFromDisk(void) {    long long start = ustime();    if (server.aof_state == REDIS_AOF_ON) {        if (loadAppendOnlyFile(server.aof_filename) == REDIS_OK)            redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);    } else {        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);        }    }}
如果沒有開啟AOF配置,那麼就會嘗試載入RDB。由rdbLoad完成RDB的載入,下面看一下這個函數。

    uint32_t dbid;    int type, rdbver;    redisDb *db = server.db+0;    char buf[1024];    long long expiretime, now = mstime();    FILE *fp;    rio rdb;    if ((fp = fopen(filename,"r")) == NULL) return REDIS_ERR;
首先是開啟RDB檔案。

    rioInitWithFile(&rdb,fp);    rdb.update_cksum = rdbLoadProgressCallback;    rdb.max_processing_chunk = server.loading_process_events_interval_bytes;
初始化rio,設定update_cksum回呼函數,以及read的塊大小(預設配置的是2M)。這裡使用update_cksum還完成一些功能:

1)更新載入進度

2)如果是主從同步過程中,載入RDB,因為整個載入過程可能會很漫長,所以需要不停的想master發送心跳,避免master認為這個slave已經timeout,主動中斷連線。

3)處理一些io事件

    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;    }
讀取RDB前9個位元組,校正magic number以及版本號碼。

    startLoading(fp);
準備開始載入,記錄載入開始時間,以及需要載入的位元組總數,用於更新載入進度。

接下來是一個解釋迴圈,不停的讀取一條條指令。

    while (1) {        // 解釋一條條指令    }
下面看一下一條指令的解釋過程:

        robj *key, *val;        expiretime = -1;        /* Read type. */        if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
首先讀取type(對應OPCode)。

        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;        }
如果OPCode對應的expire指令,需要解析出對應的expire time,然後再次讀取type(對應隨後的kv對中value的類型)。

        if (type == REDIS_RDB_OPCODE_EOF)            break;
如果OPCode對應的是EOF指令,則RDB載入完成,跳出迴圈。

        /* Handle SELECT DB opcode as a special case */        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;        }
如果OPCode是selectDB指令,讀取DB號,然後切換到對應的DB。

上面把特殊的指令執行完畢,接下來要解析KV對。

        /* Read key */        if ((key = rdbLoadStringObject(&rdb)) == NULL) goto eoferr;
讀取字串類型的key。

        /* Read value */        if ((val = rdbLoadObject(type,&rdb)) == NULL) goto eoferr;
讀取value,函數rdbLoadObject會根據type的不同,執行不同類型對象的載入,這裡不對該函數展開。

        /* 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;        }
檢測是否到期,如果到期就不會將KV對添加到DB中。

        /* Add the new object in the hash table */        dbAdd(db,key,val);
將KV對添加到DB。

        /* Set the expire time if needed */        if (expiretime != -1) setExpire(db,key,expiretime);        decrRefCount(key);
如果設定了expire time,則添加到expire dict中。

    /* Verify the checksum if RDB version is >= 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);        }    }
檢查check sum。

    fclose(fp);    stopLoading();    return REDIS_OK;
最後,關閉檔案,並將server.load置為0,表示載入不在進行。

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 */
在上述載入過程中出錯,會跳到eoferr分支。載入出錯時,列印日誌並退出進程。


redis源碼分析(7)——rdb

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.