redis源碼註釋六:對象系統

1. 對象類型和內部編碼

我們之前分析了幾種redis底層的數據結構,包括簡單字符串、雙端鏈表、字典、跳錶、整數集合、壓縮列表等,這些還不是redis的對象類型,redis的對象類型總共包含5種,分別是字符串、列表、集合、有序集合、哈希,在server.h中定義。

/* 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. */

每種對象類型在底部都有不同的實現方式,而且不止一種。
但是在執行命令時,不需要關注key的底層實現是哪一種,無論是哪一種,都會自動的調用相關的處理函數,這類似多態,要求redis在內部能夠執行類型檢查等操作。

Redis 的每一種數據類型,比如字符串、列表、有序集,它們都擁有不只一種底層實現(Redis 內部稱之爲編碼,encoding),這說明,每當對某種數據類型的鍵進行操作時,程序都必須根據鍵所採取的編碼,進行不同的操作。
比如說,集合類型就可以由字典和整數集合兩種不同的數據結構實現,但是,當用戶執行ZADD 命令時,他/她應該不必關心集合使用的是什麼編碼,只要Redis 能按照ZADD 命令的指示,將新元素添加到集合就可以了。
這說明,操作數據類型的命令除了要對鍵的類型進行檢查之外,還需要根據數據類型的不同編碼進行多態處理。——《redis設計與實現》

那麼五種數據類型的底層實現到底是什麼呢?
可以通過下面的圖大致看一下,redis-5.0.8版本和下面的圖有些差異:

在這裏插入圖片描述

1.1 字符串內部編碼

encoding 含義
OBJ_ENCODING_INT 整數
OBJ_ENCODING_RAW sds動態字符串
OBJ_ENCODING_EMBSTR Embedded sds

Embedded sds 其實就是sdshdr8。在字符串較短(長度小於44字節)的情況下會使用這種編碼方式。至於爲什麼是小於44字節的時候採用Embedded sds下面有詳細計算。

1.2 列表內部編碼

encoding 含義
OBJ_ENCODING_ZIPLIST 壓縮列表
OBJ_ENCODING_QUICKLIST 快表

新版的redis中不再使用OBJ_ENCODING_LINKEDLIST,即雙端鏈表,有點搞不懂,註釋:

#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */

QUICKLIST本身是對ZIPLIST的一種封裝,ZIPLIST之前沒有介紹,可以閱讀《redis設計與實現》。

1.3 有序集合內部編碼

encoding 含義
OBJ_ENCODING_ZIPLIST 壓縮列表
OBJ_ENCODING_SKIPLIST 跳錶

1.4 集合內部編碼

encoding 含義
OBJ_ENCODING_INTSET 整數集合
OBJ_ENCODING_HT 哈希表

1.5 哈希表內部編碼

encoding 含義
OBJ_ENCODING_ZIPLIST 壓縮列表
OBJ_ENCODING_HT 哈希表

1.6 對象結構體

根據上面的分析,對象的類型有五種,內編碼又各不相同,給定一個對象,如何知道對象的類型以及其內部編碼呢?畢竟在對 對象進行操作時肯定要根據相應的類型調用相關函數。

爲了解決上面的問題,對象的結構體中保存了類型以及內部編碼,結構體在server.h中:

typedef struct redisObject {
    unsigned type:4;		//對象類型4bit
    unsigned encoding:4;	//編碼方式4bit
    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;

對象的類型就是以下五種:

/* 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. */

編碼方式:

#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

引用計數與C++相似,對象被創建時引用計數爲1,引用計數爲0時對象被銷燬。

有了對象結構體,在使用命令操作對象時,就比較清晰了:

當執行一個處理數據類型的命令時,Redis 執行以下步驟:

  1. 根據給定key ,在數據庫字典中查找和它像對應的redisObject ,如果沒找到,就返回NULL 。
  2. 檢查redisObject 的type 屬性和執行命令所需的類型是否相符,如果不相符,返回類型錯誤。
  3. 根據redisObject 的encoding 屬性所指定的編碼,選擇合適的操作函數來處理底層的數據結構。
  4. 返回數據結構的操作結果作爲命令的返回值。

2. 對象操作

2.1 創建對象

由於對象有多種,創建對象的函數也有多個。

2.1.1 創建基本對象createObject

這個函數基本上是用來被其他創建對象的函數調用,type表示類型,編碼默認爲OBJ_ENCODING_RAW。

robj *createObject(int type, void *ptr) { //創建一個對象,傳入對象類型以及指向內部實現的指針ptr
    robj *o = zmalloc(sizeof(*o));
    o->type = type;//設置對象的類型
    o->encoding = OBJ_ENCODING_RAW;//將編碼設置默認的raw,創建相應的對象時會對該字段更改
    o->ptr = ptr;//指向內部實現
    o->refcount = 1;//引用計數爲1

    /* Set the LRU to the current lruclock (minutes resolution), or
     * alternatively the LFU counter. */
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }
    return o;
}

2.1.2 創建字符串對象

字符串的底層實現有整數、sds、EMBSTR,所以創建字符串的函數也與這三種相關。

robj *createRawStringObject(const char *ptr, size_t len) { //創建sds
    return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}

robj *createEmbeddedStringObject(const char *ptr, size_t len) { //創建EmbeddedString
    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] = '\0';
    else if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)		//小於等於44字節的使用EMBSTR,44是由64-sizeof(redisObject)-3(sds8中的3個字節)-1('\0')得來的
        return createEmbeddedStringObject(ptr,len); //sizeof(redisObject)=16,結構體對齊
    else
        return createRawStringObject(ptr,len);	//大於44字節的使用sds
}

createStringObject函數內部通過對字符串長度的判斷,選擇兩種不同的編碼方式,當字符串的長度小於等於44字節(不包含尾部的'\0')時採用EMBSTR,大於44字節採用RAW
至於爲什麼是44字節呢?我們通過結構體分析一下:
jemalloc會分配8,16,32,64等字節的內存,這裏分配內存時就是64bit。

#define LRU_BITS 24
typedef struct redisObject {
    unsigned type:4;		//對象類型4bit
    unsigned encoding:4;	//編碼方式4bit
    unsigned lru:LRU_BITS; //24bit
    int refcount;//引用計數
    void *ptr;//指向實際值的指針
} robj;

redisObject的大小是16字節(考慮到對齊),ptr指向的內容是一個sdshdr8的結構體:

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

len佔1字節,alloc佔1字節,flags佔1字節,buf中的內容44字節,尾部的'\0'佔1字節,總共佔48字節。
加上redisObject的16字節,正好64字節,這就是44的來由。

字符串編碼中還有一個沒有講到,那就是OBJ_ENCODING_INT,對於小整數,一般採用這種編碼方式。

robj *createStringObjectFromLongLongWithOptions(long long value, int valueobj) {
    robj *o;

    if (server.maxmemory == 0 ||
        !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS))
    {
        /* If the maxmemory policy permits, we can still return shared integers
         * even if valueobj is true. */
        valueobj = 0;
    }

    if (value >= 0 && value < OBJ_SHARED_INTEGERS && valueobj == 0) { //value的值在共享的範圍內而且valueobj爲0,可以返回一個共享的對象
        incrRefCount(shared.integers[value]);
        o = shared.integers[value];
    } else { //不能使用共享的對象則只好重新分配對象
        if (value >= LONG_MIN && value <= LONG_MAX) { //如果value可以使用long類型表示,則使用OBJ_ENCODING_INT編碼
            o = createObject(OBJ_STRING, NULL);
            o->encoding = OBJ_ENCODING_INT;
            o->ptr = (void*)((long)value);
        } else {
            o = createObject(OBJ_STRING,sdsfromlonglong(value));//超過long的表示範圍則將數組轉換爲字符串
        }
    }
    return o;
}

robj *createStringObjectFromLongLong(long long value) {
    return createStringObjectFromLongLongWithOptions(value,0);//如果可以,儘量創建共享對象
}

robj *createStringObjectFromLongLongForValue(long long value) {
    return createStringObjectFromLongLongWithOptions(value,1);//不使用共享對象
}

這裏需要注意共享對象的概念。

有一些對象在Redis 中非常常見,比如命令的返回值OK 、ERROR 、WRONGTYPE
等字符,另外,一些小範圍的整數,比如個位、十位、百位的整數都非常常見。 爲了利用這種常見情況,Redis 在內部使用了一個Flyweight
模式:通過預分配一些常見的值 對象,並在多個數據結構之間共享這些對象,程序避免了重複分配的麻煩,也節約了一些CPU 時間。 Redis
預分配的值對象有如下這些: • 各種命令的返回值,比如執行成功時返回的OK ,執行錯誤時返回的ERROR ,類型錯誤時
返回的WRONGTYPE ,命令入隊事務時返回的QUEUED ,等等。 • 包括0 在內,
小於redis.h/REDIS_SHARED_INTEGERS 的所有整數 (REDIS_SHARED_INTEGERS
的默認值爲10000)

2.1.3 複製字符串對象

複製返回的對象引用計數總是1。

robj *dupStringObject(const robj *o) {
    robj *d;

    serverAssert(o->type == OBJ_STRING);//不是字符串對象會報錯
	//根據不同的編碼類型創建新的對象
    switch(o->encoding) {
    case OBJ_ENCODING_RAW:
        return createRawStringObject(o->ptr,sdslen(o->ptr));
    case OBJ_ENCODING_EMBSTR:
        return createEmbeddedStringObject(o->ptr,sdslen(o->ptr));
    case OBJ_ENCODING_INT:
        d = createObject(OBJ_STRING, NULL);//對於整數對象,創建出來的總是非共享的
        d->encoding = OBJ_ENCODING_INT;
        d->ptr = o->ptr;
        return d;
    default:
        serverPanic("Wrong encoding.");
        break;
    }
}

2.1.4 創建列表對象

robj *createQuicklistObject(void) { //創建內部編碼爲快表的列表對象
    quicklist *l = quicklistCreate();
    robj *o = createObject(OBJ_LIST,l);
    o->encoding = OBJ_ENCODING_QUICKLIST;//編碼爲OBJ_ENCODING_QUICKLIST
    return o;
}

robj *createZiplistObject(void) { //創建內部編碼爲壓縮列表的列表對象
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_LIST,zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;//編碼爲OBJ_ENCODING_ZIPLIST
    return o;
}

兩種編碼方式,但是在釋放列表對象的時候,如果編碼方式不是OBJ_ENCODING_QUICKLIST將會報錯,那麼是否說明OBJ_ENCODING_ZIPLIST不能用呢?(不清楚)

2.1.5 創建set對象

robj *createSetObject(void) { //創建內部編碼爲哈希表的set對象
    dict *d = dictCreate(&setDictType,NULL);
    robj *o = createObject(OBJ_SET,d);
    o->encoding = OBJ_ENCODING_HT;
    return o;
}

robj *createIntsetObject(void) { //創建內部編碼爲intset的set對象
    intset *is = intsetNew();
    robj *o = createObject(OBJ_SET,is);
    o->encoding = OBJ_ENCODING_INTSET;
    return o;
}

2.1.6 創建哈希對象

robj *createHashObject(void) { //創建內部編碼爲壓縮列表的哈希對象
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_HASH, zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

2.1.7 創建有序集合對象

robj *createZsetObject(void) { //創建內部編碼爲跳錶的有序集合對象
    zset *zs = zmalloc(sizeof(*zs));
    robj *o;

    zs->dict = dictCreate(&zsetDictType,NULL);
    zs->zsl = zslCreate();
    o = createObject(OBJ_ZSET,zs);
    o->encoding = OBJ_ENCODING_SKIPLIST;
    return o;
}

robj *createZsetZiplistObject(void) { //創建內部編碼爲壓縮列表的有序集合對象
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_ZSET,zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

2.2 對象的釋放

void freeStringObject(robj *o) { //釋放字符串對象
    if (o->encoding == OBJ_ENCODING_RAW) {
        sdsfree(o->ptr);
    }
}

void freeListObject(robj *o) { //釋放列表對象
    if (o->encoding == OBJ_ENCODING_QUICKLIST) {
        quicklistRelease(o->ptr);
    } else {
        serverPanic("Unknown list encoding type");//編碼不是OBJ_ENCODING_QUICKLIST都報錯,OBJ_ENCODING_ZIPLIST呢?
    }
}

void freeSetObject(robj *o) { //釋放集合對象
    switch (o->encoding) {
    case OBJ_ENCODING_HT: //哈希表
        dictRelease((dict*) o->ptr);
        break;
    case OBJ_ENCODING_INTSET://整數集合
        zfree(o->ptr);
        break;
    default:
        serverPanic("Unknown set encoding type");
    }
}

void freeZsetObject(robj *o) { //釋放有序集合對象
    zset *zs;
    switch (o->encoding) {
    case OBJ_ENCODING_SKIPLIST://跳錶
        zs = o->ptr;
        dictRelease(zs->dict);
        zslFree(zs->zsl);
        zfree(zs);
        break;
    case OBJ_ENCODING_ZIPLIST://壓縮列表
        zfree(o->ptr);
        break;
    default:
        serverPanic("Unknown sorted set encoding");
    }
}

void freeHashObject(robj *o) { //釋放哈希對象
    switch (o->encoding) {
    case OBJ_ENCODING_HT: //哈希表
        dictRelease((dict*) o->ptr);
        break;
    case OBJ_ENCODING_ZIPLIST://壓縮列表
        zfree(o->ptr);
        break;
    default:
        serverPanic("Unknown hash encoding type");
        break;
    }
}

2.3 引用計數的增加與減少

void incrRefCount(robj *o) { //增加引用計數
    if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount++;
}

void decrRefCount(robj *o) { //減少引用計數,如果引用計數爲1,再減少變爲0,則會釋放對象
    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--;
    }
}

還有一些與省內存相關的函數,不再列舉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章