首頁 > 軟體

Redis物件與redisObject超詳細分析原始碼層

2022-11-28 22:02:16

以下內容是基於Redis 6.2.6 版本整理總結

一、物件

前面幾篇文章,我們介紹了Redis用到的主要的資料結構,如:sds、list、dict、ziplist、skiplist、inset等。

但是,Redis並沒有直接使用這些資料結構來實現key-value資料庫,而是基於這些資料結構構建了一個物件系統。包括字串物件、列表物件、雜湊物件、集合物件和有序集合物件五種型別的物件。每種物件都使用了至少一種前面提到的資料結構。

通過對物件的區分,Redis可以在執行命令前判斷該物件是否能夠執行該條命令。為物件設定不同的資料結構實現,只要是為了提高效率。

二、物件的型別及編碼

Redis使用物件來表示資料中的key和value,每當我們在Redis資料庫中建立一個新的鍵值對時,至少會建立兩個物件,一個作用域key,另一個作用於value。

舉個栗子:set msg “hello world” 表示分別建立了一個字串物件儲存“msg”,另一個字串物件儲存“hello world”:

redisObject 結構體

Redis中的每個物件由 redisObject 結構體來描述,物件的型別、編碼、記憶體回收、共用物件都需要redisObject的支援,redisObject 結構體定義如下:

#define LRU_BITS 24
typedef struct redisObject {
    unsigned type:4;   // 型別
    unsigned encoding:4; // 編碼
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

下面我們來看看每個欄位的含義:

(1)type: 佔4個位元位,表示物件的型別,有五種型別。當我們執行type命令時,便是通過type欄位獲取物件的型別。

/* The actual Redis Object */
#define OBJ_STRING 0    /* String object. */
#define OBJ_LIST 1      /* List object. */
#define OBJ_SET 2       /* Set object. */
#define OBJ_ZSET 3      /* Sorted set object. */
#define OBJ_HASH 4      /* Hash object. */

type命令使用範例:

(2)encoding: 佔4個位元位,表示物件使用哪種編碼,redis會根據不同的場景使用不同的編碼,大大提高了redis的靈活性和效率。

字串物件不同編碼型別範例:

(3)lru: 佔 24 個位元位,記錄該物件最後一次被存取的時間。千萬別以為這隻能在LRU淘汰策略中才用,LFU也是複用的個欄位。當使用LRU時,它儲存的上次讀寫的24位元unix時間戳(秒級);使用LFU時,24位元會被分為兩個部分,16位元的分鐘級時間戳和8位元特殊計數器,這裡就不展開了,詳細可以注意我後續的文章。

(4)refcount: 物件的參照計數,類似於shared_ptr 智慧指標的參照計數,當refcount為0時,釋放該物件。

(5)ptr: 指向物件具體的底層實現的資料結構。

三、不同物件編碼規則

四、redisObject結構各欄位使用範例

Redis中操作key的命令大致可以分為兩類:一種是可以操作任何型別的key,如:del type object等等,另外一種是針對特定型別的key只能使用特定的命令。如:LLEN只能用來獲取列表物件的長度。

4.1 型別檢查(type欄位)

比如對於LLEN命令,Redis伺服器在執行命令之前會先檢查輸入的key對應的的value物件是否為列表型別,即檢查該value物件的type型別是不是OBJ_LIST,如果是才會執行LLEN命令。否則就拒絕執行命令並返回操作型別錯誤。

4.2 多型命令的實現(encoding)

Redis除了會根據value物件的型別來判斷對應key能否執行執行命令外,還會根據value物件的**編碼方式(encoding欄位)**選擇正確的方式來執行命令。比如:列表物件的編碼方式有quicklist 和 ziplist兩種,Redis伺服器除了判斷對應value物件的型別為列表物件還要根據具體的編碼選擇正確的LLEN執行。

借用物件導向的術語來說,可以認為LLEN命令是多型的。只要執行LLEN命令的列表鍵,無論value物件的編碼是哪種方式,LLEN命令都可以正常執行。實際上del type 等也是多型命令。他們和LLEN的區別在於,前者是基於型別的多型,後者是基於編碼的多型。

4.3 記憶體回收和共用物件(refcount)

C語言不具備自動回收功能,Redis就通過參照計數實現了自己的記憶體回收機制。具體是由redisObject結構中的refcount欄位記錄。物件的參照計數會隨著物件的使用狀態而不斷變化。

建立一個新物件時,refcount會被初始化為1,;當物件被另一個新程式使用時 refcount加1;不被一個程式使用時減1;當refcount==0時,該物件所佔的空間會被回收。

參照計數除了被用來實現記憶體回收外,還被用來實現物件共用。比如:

上面我們建立可一個value為100的key A,並使用object refcount來檢視key A的參照計數,會看到此時的refcount為2,這是為什麼呢?此時有兩個地方參照了這個value物件,一個是持有該物件的伺服器程式,另外一個是共用該value物件的key A。如果,我們再建立一個value為100 的 key B,那麼鍵B也會指向這個value物件,使得該物件的參照計數變為3。由此,可以看到,共用value物件的鍵越多,節約的記憶體就越多。

在建立鍵B的時候,伺服器考到鍵B要建立的物件是int編碼的字串物件100,而剛好有個value為100的共用物件匹配,就直接將鍵B指向該共用物件。因為是整數的字串物件,直接比較即可,如果共用物件是字串值的物件,要從頭到尾比較每個字元,時間複雜度O(n)。

簡單來說就是,要能使用共用物件,需要先驗證該共用物件和要建立的目標物件是不是完全一致,如果共用物件儲存的值越複雜,消耗的CPU也就越多,所以Redis值對整數型別的字串物件做了共用。沒有共用儲存字串值的字串物件。

Redis在初始化伺服器是,就建立了一萬個字串物件,這些物件包含了0~9999的所有整數值。當你建立了這個範圍內的 字串物件時,伺服器就會使用這些共用物件,而不是建立新物件,以節約記憶體。

4.4 物件的空轉時長(lru)

redisObject結構中的lru欄位儲存,該物件最後一次被存取的時間。 使用object idletime 來檢視,注意這個命令不會修改物件的lru屬性。

當Redis開啟最大記憶體限制,一般為機器記憶體的一半,如果redis使用的記憶體達到這個值,且記憶體淘汰策略使用的是volatile-lru 或者 allkeys-lru,空轉時長較高的那些鍵會被優先釋放。

使用object idletime 檢視鍵的空間時間,單位:秒:

127.0.0.1:6379[1]> keys *
1) "msg"
2) "teacher"
127.0.0.1:6379[1]> object idletime msg
(integer) 71166
127.0.0.1:6379[1]>

五、物件在原始碼中的使用

5.1 字串物件

5.1.1字串物件建立

// code location: src/object.c  
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
// 建立 strintg 物件
robj *createStringObject(const char *ptr, size_t len) {
	// 如果待儲存的字串的長度小於等於44,使用 embstr 編碼
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else // 否則使用 raw 編碼
        return createRawStringObject(ptr,len);
}
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
	// 申請 robj + sdshdr + data + 1 的空間
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    struct sdshdr8 *sh = (void*)(o+1);
    o->type = OBJ_STRING;      // 設定型別
    o->encoding = OBJ_ENCODING_EMBSTR; // 設定編碼
    o->ptr = sh+1;
    o->refcount = 1;
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }
    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr == SDS_NOINIT)
        sh->buf[len] = '';
    else if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

從 createEmbeddedStringObject 函數可以看到,該物件是robj和sds的結合體,將sds直接放入到robj裡,這也是嵌入式編碼embstr的由來。

為什麼要限制44位元組呢?因為robj結構體佔16個位元組,sdshdr結構體佔3個位元組,最後結尾的‘’佔一個位元組,限制44個位元組,就能保證64個位元組裡能放下所有內容(16+3+1+44 = 64)。

5.1.2 字串物件編碼

Redis將節省記憶體做到了極致,它的作者對字串物件又做了特殊的編碼處理,以進一步達到節省空間的目的。編碼處理過程及程式碼註釋如下:

/* Try to encode a string object in order to save space */
robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;
    size_t len;
    /* Make sure this is a string object, the only type we encode
     * in this function. Other types use encoded memory efficient
     * representations but are handled by the commands implementing
     * the type. */
    // 這裡只對string物件進行編碼,其他型別的編碼都有對應的具體實現處理
    serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
    /* We try some specialized encoding only for objects that are
     * RAW or EMBSTR encoded, in other words objects that are still
     * in represented by an actually array of chars. */
    // 非sds string物件,直接返回原物件
    if (!sdsEncodedObject(o)) return o;
    /* It's not safe to encode shared objects: shared objects can be shared
     * everywhere in the "object space" of Redis and may end in places where
     * they are not handled. We handle them only as values in the keyspace. */
    // 如果該物件由其他物件共用,不能編碼,如果編碼可能影響到其他物件的使用
     if (o->refcount > 1) return o;
    /* Check if we can represent this string as a long integer.
     * Note that we are sure that a string larger than 20 chars is not
     * representable as a 32 nor 64 bit integer. */
    // 檢查能否把一個字串表示為長整型數,長度要小於等於20
    len = sdslen(s);
    if (len <= 20 && string2l(s,len,&value)) {
        /* This object is encodable as a long. Try to use a shared object.
         * Note that we avoid using shared integers when maxmemory is used
         * because every object needs to have a private LRU field for the LRU
         * algorithm to work well. */
         // 如果可以被編碼為long型,且編碼後的值小於OBJ_SHARED_INTEGERS(10000),且未配
         // 置LRU替換淘汰策略, 就使用這個數的共用物件,相當於所有小於10000的數都是用的同一個robj
        if ((server.maxmemory == 0 ||
            !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
            value >= 0 &&
            value < OBJ_SHARED_INTEGERS)
        {
            decrRefCount(o);
            incrRefCount(shared.integers[value]);
            return shared.integers[value];
        } else {
            if (o->encoding == OBJ_ENCODING_RAW) {
                sdsfree(o->ptr);
                o->encoding = OBJ_ENCODING_INT;
                o->ptr = (void*) value;
                return o;
            } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
                decrRefCount(o);
                return createStringObjectFromLongLongForValue(value);
            }
        }
    }
    // 不能轉為long的字串
    /* If the string is small and is still RAW encoded,
     * try the EMBSTR encoding which is more efficient.
     * In this representation the object and the SDS string are allocated
     * in the same chunk of memory to save space and cache misses. */
    // 如果字串的長度太小,小於等於44
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;
        // 如果當前編碼是embstr,直接返回原物件,否則轉為embstr編碼,返回
        if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }
    /* We can't encode the object...
     *
     * Do the last try, and at least optimize the SDS string inside
     * the string object to require little space, in case there
     * is more than 10% of free space at the end of the SDS string.
     *
     * We do that only for relatively large strings as this branch
     * is only entered if the length of the string is greater than
     * OBJ_ENCODING_EMBSTR_SIZE_LIMIT. */
    // 如果前面編碼沒有成功,這裡做最後一步,當編碼型別為raw,且它的sds可用空間超過10%,
    // 嘗試釋放調這部分記憶體
    trimStringObjectIfNeeded(o);
    /* Return the original object. */
    // 返回原物件
    return o;
}

5.1.3 字串物件解碼

有編碼就有解碼,實際上只需要那些可以轉為整型型別的字串傳進行解碼,解碼程式碼及註釋如下:

robj *getDecodedObject(robj *o) {
    robj *dec;
	// 如果編碼是 embstr 和 raw ,只是把參照計數加一,然後返回原物件
    if (sdsEncodedObject(o)) {
        incrRefCount(o);
        return o;
    }
    // 如果編碼是 int 進行解碼,返回新的物件
    if (o->type == OBJ_STRING && o->encoding == OBJ_ENCODING_INT) {
        char buf[32];
        ll2string(buf,32,(long)o->ptr);
        dec = createStringObject(buf,strlen(buf));
        return dec;
    } else {
        serverPanic("Unknown encoding type");
    }
}

5.1.4 redis物件參照計數及自動清理

void incrRefCount(robj *o) {
    if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT) {
        o->refcount++; // 參照計數加一
    } else {
        if (o->refcount == OBJ_SHARED_REFCOUNT) {
            /* Nothing to do: this refcount is immutable. */
        } else if (o->refcount == OBJ_STATIC_REFCOUNT) {
            serverPanic("You tried to retain an object allocated in the stack");
        }
    }
}
// 減少參照計數
void decrRefCount(robj *o) {
	// 釋放空間
    if (o->refcount == 1) {
        switch(o->type) {
        case OBJ_STRING: freeStringObject(o); break;
        case OBJ_LIST: freeListObject(o); break;
        case OBJ_SET: freeSetObject(o); break;
        case OBJ_ZSET: freeZsetObject(o); break;
        case OBJ_HASH: freeHashObject(o); break;
        case OBJ_MODULE: freeModuleObject(o); break;
        case OBJ_STREAM: freeStreamObject(o); break;
        default: serverPanic("Unknown object type"); break;
        }
        zfree(o);
    } else {
        if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
        if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--; // 計數減一
    }
}

六、總結

  • redis物件為所有型別的value提供了統一的封裝
  • 為物件的淘汰策略儲存相關資訊
  • 實現參照計數及記憶體自動釋放功能

到此這篇關於Redis物件與redisObject超詳細分析原始碼層的文章就介紹到這了,更多相關Redis物件與redisObject內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com