1. 程式人生 > >redis資料庫之rdb持久化

redis資料庫之rdb持久化

redis是一種記憶體資料庫,也就是redis的資料在正常工作的情況下都是儲存在記憶體中。但並不是說redis只能把資料儲存在記憶體中,redis提供了兩種資料持久化機制:rdb和aof。rdb持久化有三種方式被啟動:使用者向redis傳送save或者bgsave命令。save和bgsave的不同就在於save會阻塞redis伺服器,而bgsave不會。這樣bgsave就在不影響redis伺服器正常工作的情況進行資料持久化,bgsave主要是通過子程序來進行資料持久化。無論是save還是bgsave最後都會通過呼叫rdbsave來進行儲存操作。

在講解rdbsave這個函式之前,先來介紹下rdb檔案格式。一個rdb檔案分為以下幾個部分:


REDIS:檔案的最開頭儲存著REDIS 五個字元,標識著一個RDB 檔案的開始。

RDB-VERSION:一個四位元組長的以字元表示的整數,記錄了該檔案所使用的RDB 版本號。目前的RDB 檔案版本為0006 。

SELECT-DB:這域儲存著跟在後面的鍵值對所屬的資料庫號碼。在讀入RDB 檔案時,程式會根據這個域的值來切換資料庫,確保資料被還原到正確的資料庫上。

KEY-VALUE-PAIRS:每個鍵值對的資料使用以下結構來儲存:


OPTIONAL-EXPIRE-TIME:,如果鍵沒有設定過期時間,那麼這個域就不會出現;反之,如果這個域出現的話,那麼它記錄著鍵的過期時間

KEY:儲存著鍵,格式和REDIS_ENCODING_RAW 編碼的字串物件一樣

TYPE-OF-VALUE:記錄著VALUE 域的值所使用的編碼,根據這個域的指示,程式會使用不同的方式來儲存和讀取VALUE 的值。

VALUE:儲存著真實的值,但是這個被儲存的值會被進行各種編碼。(可以檢視redis設計與實現)

下面來正式看下rdbsave函式:

int rdbSave(char *filename) 
{
......
// 以 "temp-<pid>.rdb" 格式建立臨時檔名
    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");
......
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;
        // 指向資料庫 key space
        dict *d = db->dict;
        // 資料庫為空, pass ,處理下個數據庫
        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 */
        // 將資料庫中的所有節點儲存到 RDB 檔案
        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);
    }
......
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
......
}
rdbsave的整體邏輯還是比較簡單的。我們具體還是來看rdb是怎麼儲存redis各種型別的。

儲存type的函式rdbSaveType的邏輯很簡單,就不介紹了。儲存len的方式在講壓縮列表的時候介紹過,這裡也不介紹了。

來看一個比較重要的函式:rdbSaveKeyValuePair

int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
                        long long expiretime, long long now)
{
             if (expiretime != -1) {
                    .......
             }
......
              // 儲存值型別
              if (rdbSaveObjectType(rdb,val) == -1) return -1;
               // 儲存 key
               if (rdbSaveStringObject(rdb,key) == -1) return -1;
               // 儲存 value
               if (rdbSaveObject(rdb,val) == -1) return -1;
......
}
這個函式就是儲存db-data的。主要就是儲存過期時間,以及值的型別,key以及值。rdbSaveObjectType也比較簡單,就不做介紹了。
int rdbSaveStringObject(rio *rdb, robj *obj) {
    /* Avoid to decode the object, then encode it again, if the
     * object is alrady integer encoded. */
    if (obj->encoding == REDIS_ENCODING_INT) {
        // 整數在嘗試編碼之後寫入
        return rdbSaveLongLongAsStringObject(rdb,(long)obj->ptr);
    } else {
        // 如果是字串,直接寫入 rdb
        redisAssertWithInfo(NULL,obj,obj->encoding == REDIS_ENCODING_RAW);
        return rdbSaveRawString(rdb,obj->ptr,sdslen(obj->ptr));
    }
}
如果是整數會進行編碼後再寫入,而字串直接呼叫rdbSaveRawString寫入到rdb中。先來看下rdbSaveLongLongAsStringObject:
/* Save a long long value as either an encoded string or a string. */
/*
 * 將一個 long long 值儲存為字串,或者編碼字串
 */
int rdbSaveLongLongAsStringObject(rio *rdb, long long value) {
    unsigned char buf[32];
    int n, nwritten = 0;
    // 嘗試進行編碼
    int enclen = rdbEncodeInteger(value,buf);
    if (enclen > 0) {
        // 編碼成功
        return rdbWriteRaw(rdb,buf,enclen);
    } else {
        /* Encode as string */
        // 編碼失敗,將整數儲存為字串
        enclen = ll2string((char*)buf,32,value);
        redisAssert(enclen < 32);
        if ((n = rdbSaveLen(rdb,enclen)) == -1) return -1;
        nwritten += n;
        if ((n = rdbWriteRaw(rdb,buf,enclen)) == -1) return -1;
        nwritten += n;
    }

    return nwritten;
}
首先會進行編碼,如果編碼失敗,會再以字串的方式寫入rdb檔案中。主要來看rdbEncodeInteger:
int rdbEncodeInteger(long long value, unsigned char *enc) {
    if (value >= -(1<<7) && value <= (1<<7)-1) {
        enc[0] = (REDIS_RDB_ENCVAL<<6)|REDIS_RDB_ENC_INT8;
        enc[1] = value&0xFF;
        return 2;
    } else if (value >= -(1<<15) && value <= (1<<15)-1) {
        enc[0] = (REDIS_RDB_ENCVAL<<6)|REDIS_RDB_ENC_INT16;
        enc[1] = value&0xFF;
        enc[2] = (value>>8)&0xFF;
        return 3;
    } else if (value >= -((long long)1<<31) && value <= ((long long)1<<31)-1) {
        enc[0] = (REDIS_RDB_ENCVAL<<6)|REDIS_RDB_ENC_INT32;
        enc[1] = value&0xFF;
        enc[2] = (value>>8)&0xFF;
        enc[3] = (value>>16)&0xFF;
        enc[4] = (value>>24)&0xFF;
        return 5;
    } else {
        return 0;
    }
}
如果整數小於2的31次方減一而且大於負的2的31次方都會進行整數編碼, 如果不在這個範圍就會按字串寫入。而這個範圍又分為幾個小範圍都是按char,short,int的最大值和最小值來判斷的。

如果vale在char範圍內,字元陣列的第一個元素為:C0也就是11000000

如果value在short範圍內,字元陣列的第一個元素為:C1也就是11000001

如果value在int範圍內,字元陣列的第一個元素為:C2也就是11000010

字元陣列後面都是真實值。

繼續來看另外一個比較重要的函式:

int rdbSaveRawString(rio *rdb, unsigned char *s, size_t len) 
{
......
if (len <= 11) {
        unsigned char buf[5];
        if ((enclen = rdbTryIntegerEncoding((char*)s,len,buf)) > 0) {
            if (rdbWriteRaw(rdb,buf,enclen) == -1) return -1;
            return enclen;
        }
    }
......
if (server.rdb_compression && len > 20) {
}
......
if ((n = rdbSaveLen(rdb,len)) == -1) return -1;
    nwritten += n;
    if (len > 0) {
        if (rdbWriteRaw(rdb,s,len) == -1) return -1;
        nwritten += len;
    }
......
}
字串的編碼會分一下幾種情況:

1、如果字串的長度小於11,會把字串轉化為整形,在寫入rdb中。

2、如果需要lzf壓縮,會進行壓縮後,把壓縮後的資料寫入rdb中,壓縮後是按什麼方式寫入的呢?格式如下:

首先寫入lzf的標識,也就是繼續上面value編碼的值繼續即C3(11000011).

然後是資料壓縮後的長度

再是壓縮之前的長度

最後是壓縮後的資料。

3、如果上面兩個條件都不滿足,就會按原始方式寫入。

最後來看下另外一個重要的函式,也就是真正把資料庫的值寫入rdb檔案的函式:

int rdbSaveObjectType(rio *rdb, robj *o) {

    switch (o->type) {
    // 字串
    case REDIS_STRING:
        return rdbSaveType(rdb,REDIS_RDB_TYPE_STRING);
    // 列表
    case REDIS_LIST:
        // ziplist 編碼
        if (o->encoding == REDIS_ENCODING_ZIPLIST)
            return rdbSaveType(rdb,REDIS_RDB_TYPE_LIST_ZIPLIST);
        // 雙端連結串列
        else if (o->encoding == REDIS_ENCODING_LINKEDLIST)
            return rdbSaveType(rdb,REDIS_RDB_TYPE_LIST);
        else
            redisPanic("Unknown list encoding");
    // 集合
    case REDIS_SET:
        // intset
        if (o->encoding == REDIS_ENCODING_INTSET)
            return rdbSaveType(rdb,REDIS_RDB_TYPE_SET_INTSET);
        // 字典
        else if (o->encoding == REDIS_ENCODING_HT)
            return rdbSaveType(rdb,REDIS_RDB_TYPE_SET);
        else
            redisPanic("Unknown set encoding");
    // 有序集 
    case REDIS_ZSET:
        // ziplist
        if (o->encoding == REDIS_ENCODING_ZIPLIST)
            return rdbSaveType(rdb,REDIS_RDB_TYPE_ZSET_ZIPLIST);
        // 跳躍表
        else if (o->encoding == REDIS_ENCODING_SKIPLIST)
            return rdbSaveType(rdb,REDIS_RDB_TYPE_ZSET);
        else
            redisPanic("Unknown sorted set encoding");
    // 雜湊
    case REDIS_HASH:
        // ziplist
        if (o->encoding == REDIS_ENCODING_ZIPLIST)
            return rdbSaveType(rdb,REDIS_RDB_TYPE_HASH_ZIPLIST);
        // 字典
        else if (o->encoding == REDIS_ENCODING_HT)
            return rdbSaveType(rdb,REDIS_RDB_TYPE_HASH);
        else
            redisPanic("Unknown hash encoding");
    default:
        redisPanic("Unknown object type");
    }

    return -1; /* avoid warning */
}
這個函式的switch-case結構跟我們在講述redis的object物件的時候那張圖剛好形成對比。不論是string型別還是skiplist又或者是hash都是通過rdbSaveRawString又或者是rdbSaveStringObject來寫入rdb中。具體什麼型別用什麼方式寫入,通過上面這個函式一眼就能看出來。

redis定時rdb持久化的方式通過serverCron函式實現,redis所有的定時任務都是通過serverCron來實現的。具體實現看serverCron,邏輯也是比較簡單,最後還是通過呼叫rdbSave寫入rdb檔案的。