1. 程式人生 > >Redis原始碼剖析(十)--RDB持久化

Redis原始碼剖析(十)--RDB持久化

RDB觸發機制

命令觸發

  • SAVE:SAVE命令會阻塞Redis服務程序,知道RDB檔案建立完畢為止。
  • BGSAVE:BGSAVE會建立子程序,子程序負責建立RDB檔案,父程序繼續處理命令請求

自動間隔性儲存

當配置檔案中save選項的條件滿足時,伺服器自動執行BGSAVE命令。

// 滿足以上三個條件中的任意一個,則自動觸發 BGSAVE 操作 
save 900 1       // 伺服器在900秒之內,對資料庫執行了至少1次修改 
save 300 10      // 伺服器在300秒之內,對資料庫執行了至少10修改 
save 60  1000    // 伺服器在60秒之內,對資料庫執行了至少1000修改 

自動執行 BGSAVE 命令的條件儲存在 redisServer 結構中的 savaparams 屬性。當伺服器成功執行一個修改命令後,dirty 計數會加一,而 lastsave屬性記錄了最後一次完成 SAVE 的 UNIX 時間戳。Redis的週期性操作函式 serverCron 會定時檢查 save 選項的條件是否滿足,如果滿足,就會執行BGSAVE命令。

struct redisServer{

    // 記錄了BGSAVE自動執行的條件
    struct saveparam *saveparams;

    // 自從上次 SAVE 執行以來,資料庫被修改的次數
long long dirty; // 最後一次完成 SAVE 的時間 time_t lastsave; // ....... } // 伺服器的儲存條件(BGSAVE 自動執行的條件) struct saveparam { // 多少秒之內 time_t seconds; // 發生多少次修改 int changes; };

 

RDB持久化原始碼

BGSAVE 命令底層實現函式 dbSaveBackground 會先 fork 出子程序,由子程序執行 rdbSave 函式。整個持久化函式 rdbSave 的核心在於通過 rdbSaveKeyValuePair 函式儲存資料庫的鍵值對,rdbSaveKeyValuePair 函式底層的 rdbSaveObject 函式會針對不用的物件型別採用不用的編碼格式來儲存資料。

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

    // 初始化 I/O
    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;

    // 遍歷所有資料庫
    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;
        }

        /* 
         * 寫入 DB 選擇器
         */
        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; 

     // 寫入 EOF 程式碼
    if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;

    /*
     * CRC64 校驗和。
     * 如果校驗和功能已關閉,那麼 rdb.cksum 將為 0 ,
     * 在這種情況下, RDB 載入時會跳過校驗和檢查。
     */
    cksum = rdb.cksum;
    memrev64ifbe(&cksum);
    rioWrite(&rdb,&cksum,8);

    // 沖洗快取,確保資料已寫入磁碟
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    /* 
     * 使用 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;
}

 

RDB檔案結構

  • REDIS:5位元組,儲存著 "REDIS" 五個字元
  • db_version:4位元組,RDB檔案的版本號
  • database 0:資料庫中的鍵值對
    • SELECTDB:1位元組常量
    • db_number:資料庫號碼
    • key_value_pairs:鍵值對
      • 含過期時間的鍵值對會帶有 EXPIRETIME_MS 和過期時間
  • EOF:RDB檔案的結束標誌
  • check_sum:校驗和(CRC64),用來檢查RDB檔案是否出錯

key_value_pairs鍵值對中的 TYPE 屬性:記錄類物件的編碼型別,程式會根據 TYPE 屬性來決定如何讀入和解釋value資料。