Redis數據編碼方式詳解

摘要: ## 引言 Redis是一個key-value存儲系統。和Memcached類似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set有序集合)和hash(哈希類型)。這些數據類型都支持push/pop、add/remove以及取交集並集和差集及更豐富的操作,而且這些操作都是原子性的。 本文將對Redis數據的

引言

Redis是一個key-value存儲系統。和Memcached類似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set有序集合)和hash(哈希類型)。這些數據類型都支持push/pop、add/remove以及取交集並集和差集及更豐富的操作,而且這些操作都是原子性的。

本文將對Redis數據的編碼方式和底層數據結構進行分析和介紹,幫助讀者更好的瞭解和使用它們。

數據類型和編碼方式

Redis中數據對象的定義如下:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;
    void *ptr;
} robj;

其中type對應了Redis中的五種基本數據類型:

\#define OBJ_STRING 0
\#define OBJ_LIST 1
\#define OBJ_SET 2
\#define OBJ_ZSET 3
\#define OBJ_HASH 4

encoding則對應了 Redis 中的十種編碼方式:

/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this 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 /* Encoded as regular linked list */
\#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 */

type和encoding的對應關係如下圖:
redis_type_encode.png

OBJ_ENCODING_RAW

RAW編碼方式使用簡單動態字符串來保存字符串對象,其具體定義爲:

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

從len字段可以判斷並不不依賴於'\0',故可以用與保存二進制對象。

從free字段可以判斷其空間分配是採用預分配的方式,避免字符串修改時頻繁分配釋放內存。具體分配算法爲:

sds sdsMakeRoomFor(sds s,size_t addlen) {
    ...
    if (sdsavail(s) >= addlen) return s;
    newlen = sdslen(s)+addlen;
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    sh = (void*) (s-(sizeof(struct sdshdr)));
    newsh = zrealloc(sh,sizeof(struct sdshdr)+newlen+1);
    if (newsh == NULL) return NULL;

    newsh->free = newlen - len;
    return newsh->buf;
}

OBJ_ENCODING_INT

INT編碼方式以整數保存字符串數據,僅限能用long類型值表達的字符串。

當robj中的LRU值沒有意義的時候(實例沒有設置maxmemory限制或者maxmemory-policy設置的淘汰算法中不計算LRU值時), 0-10000之間的OBJ_ENCODING_INT編碼的字符串對象將進行共享。

具體算法如下:

len = sdslen(s);
if (len <= 21 && 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. */
    if ((server.maxmemory == 0 ||
         (server.maxmemory_policy != MAXMEMORY_VOLATILE_LRU &&
          server.maxmemory_policy != MAXMEMORY_ALLKEYS_LRU)) &&
        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;
    }
}

OBJ_ENCODING_EMBSTR

從Redis 3.0版本開始字符串引入了EMBSTR編碼方式,長度小於OBJ_ENCODING_EMBSTR_SIZE_LIMIT的字符串將以EMBSTR方式存儲。

EMBSTR方式的意思是 embedded string ,字符串的空間將會和redisObject對象的空間一起分配,兩者在同一個內存塊中。

Redis中內存分配使用的是jemalloc,jemalloc分配內存的時候是按照8,16,32,64作爲chunk的單位進行分配的。爲了保證採用這種編碼方式的字符串能被jemalloc分配在同一個chunk中,該字符串長度不能超過64,故字符串長度限制OBJ_ENCODING_EMBSTR_SIZE_LIMIT = 64 - sizeof('\0') - sizeof(robj)爲16 - sizeof(struct sdshdr)爲8 = 39。

採用這個方式可以減少內存分配的次數,提高內存分配的效率,降低內存碎片率。

robj *createStringObject(const char *ptr,size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

robj *createEmbeddedStringObject(const char *ptr,size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    ...
  if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

OBJ_ENCODING_ZIPLIST

鏈表(List),哈希(Hash),有序集合(Sorted Set)在成員較少,成員值較小的時候都會採用壓縮列表(ZIPLIST)編碼方式進行存儲。

這裏成員"較少",成員值"較小"的標準可以通過配置項進行配置:

hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

壓縮列表簡單來說就是一系列連續的內存數據塊,其內存利用率很高,但增刪改查效率較低,所以只會在成員較少,值較小的情況下使用。

其典型結構如下:

area        |<---- ziplist header ---->|<----------- entries ------------->|<-end->|
size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN | zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |   ZIPLIST_ENTRY_END
                                                                  |
                                                         ZIPLIST_ENTRY_TAIL

area        |<------------------- entry -------------------->|
            +------------------+----------+--------+---------+
component   | pre_entry_length | encoding | length | content |
            +------------------+----------+--------+---------+

OBJ_ENCODING_LINKDEDLIST / OBJ_ENCODING_QUICKLIST

在Redis 3.2版本之前,一般的鏈表使用LINKDEDLIST編碼。

在Redis 3.2版本開始,所有的鏈表都是用QUICKLIST編碼。

兩者都是使用基本的雙端鏈表數據結構,區別是QUICKLIST每個節點的值都是使用ZIPLIST進行存儲的。

// 3.2版本之前
typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr,void *key);
    unsigned long len;
} list;

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

// 3.2版本
typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned int len;           /* number of quicklistNodes */
    int fill : 16;              /* fill factor for individual nodes */
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

OBJ_ENCODING_INTSET

整數集合(intset)是集合鍵的底層實現之一:

當一個集合只包含整數值元素, 並且這個集合的元素數量不多時, Redis 就會使用整數集合作爲集合鍵的底層實現。

robj *setTypeCreate(robj *value) {
    if (isObjectRepresentableAsLongLong(value,NULL) == REDIS_OK)
        return createIntsetObject();
    return createSetObject();
}

int setTypeAdd(robj *subject,robj *value) {
    ...
    if (subject->encoding == REDIS_ENCODING_INTSET) {
        if (isObjectRepresentableAsLongLong(value,&llval) == REDIS_OK) {
            ...
        } else {
            // 當往INTSET裏面插入非整數值時,會將集合轉成普通的HT編碼方式
            setTypeConvert(subject,REDIS_ENCODING_HT);
            ...
        }
        ...
    }
    ...
}

OBJ_ENCODING_HT

字典是Redis中存在最廣泛的一種數據結構不僅在哈希對象,集合對象和有序結合對象中都有使用,而且Redis所有的Key,Value都是存在db->dict這張字典中的。

Redis 的字典使用哈希表作爲底層實現。

一個哈希表裏面可以有多個哈希表節點,而每個哈希表節點就保存了字典中的一個鍵值對。

哈希表的定義如下:

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // Redis 的哈希表使用鏈地址法(separate chaining)來解決鍵衝突:
    // 每個哈希表節點都有一個 next 指針, 多個哈希表節點可以用 next 指針構成一個單向鏈表,
    // 被分配到同一個索引上的多個節點可以用這個單向鏈表連接起來, 這就解決了鍵衝突的問題。
    struct dictEntry *next;
} dictEntry;

/* This is our hash table structure. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dictType {
    // 用於計算Hash的函數指針
    unsigned int (*hashFunction)(const void *key);
    void *(*keyDup)(void *privdata,const void *key);
    void *(*valDup)(void *privdata,const void *obj);
    int (*keyCompare)(void *privdata,const void *key1,const void *key2);
    void (*keyDestructor)(void *privdata,void *key);
    void (*valDestructor)(void *privdata,void *obj);
} dictType;

Redis計算Hash值和索引的方式爲:

hash = dict->type->hashFunction(key);
index = hash & dict->ht[x].sizemask;

一個字典裏面保存了兩個hash表結構,用於哈希表的擴展收縮操作(Rehash)。

/* Every dictionary has two of this as we
 * implement incremental rehashing,for the old to the new table.
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;

隨着操作的不斷執行,哈希表保存的鍵值對會逐漸地增多或者減少,爲了讓哈希表的負載(used/size)維持在一個合理的範圍之內,當哈希表保存的鍵值對數量太多或者太少時,程序需要對哈希表的大小進行相應的擴展或者收縮。

具體算法爲:

  • 爲字典的 ht[1] 哈希表分配空間,這個哈希表的空間大小爲: 第一個大於等於(擴展時)或者小於等於(收縮時) ht[0].used * 2 的 2^n(2 的 n 次方冪);
  • 將保存在 ht[0] 中的所有鍵值對 rehash 到 ht[1] 上面:rehash指的是重新計算鍵的哈希值和索引值,然後將鍵值對放置到 ht[1]哈希表的指定位置上;
  • 當 ht[0] 包含的所有鍵值對都遷移到了 ht[1] 之後(ht[0] 變爲空表),釋放 ht[0] ,將 ht[1] 設置爲 ht[0],並在 ht[1] 新創建一個空白哈希表,爲下一次 rehash 做準備。

一次性完成 rehash 過程時間可能很長,Redis採用漸進式 rehash 的方式完成整個過程。

  • 每次對字典執行添加、刪除、查找或者更新操作時,程序除了執行指定的操作以外,還會順帶將 ht[0] 哈希表在 rehashidx 索引上的所有鍵值對 rehash 到 ht[1] , 並將 rehashidx 的值增一;
  • 直到整個 ht[0] 全部完成 rehash 後,rehashindex設爲-1,釋放 ht[0] , ht[1]置爲 ht[0], 在 ht[1] 創建一個新的空白表。

OBJ_ENCODING_SKIPLIST

跳躍表(SKIPLIST)編碼方式爲有序集合對象專用,有序集合對象採用了字典+跳躍表的方式實現。其定義如下:

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

其中字典裏面保存了有序集合中member與score的鍵值對,跳躍表則用於實現按score排序的功能。

跳躍表以有序的方式在層次化的鏈表中保存元素,在一般情況下情況下效率和平衡樹媲美, 比起平衡樹來說,跳躍表的實現要簡單直觀得多。
一般的跳躍表結構如下圖所示:
skiplist.png
跳躍表的基本原理是每一個節點創建的時候層數爲隨機值,層數越高的機率越小,Redis中每升高一層的概率爲1/4。也就是說,一個跳躍表裏,一般情況下,每一層的節點數爲下一次的 1/4,相當於是一個“隨機化”的平衡樹。

\#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */ macro 

int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

具體的算法在此就不詳細贅述了,與一般的跳躍表實現相比,有序集合中的跳躍表有以下特點:

  • 允許重複的 score 值:多個不同的 member 的 score 值可以相同。
  • 進行對比操作時,不僅要檢查 score 值,還要檢查 member:當 score 值可以重複時,單靠 score 值無法判斷一個元素的身份,所以需要連 member 域都一併檢查才行。
  • 每個節點都帶有一個高度爲1層的後退指針,用於從表尾方向向表頭方向迭代:當執行 ZREVRANGE 或ZREVRANGEBYSCORE這類以逆序處理有序集的命令時,就會用到這個屬性。
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    robj *obj;
    double score;
    // 後退指針,方便 zrev 系列的逆序操作
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header,*tail;
    unsigned long length;
    int level;
} zskiplist;

結束

知其然,更要知其所以然。

只有深入瞭解了Redis數據的編碼方式和底層數據結構,我們才能更好的瞭解使用它。在做疑難問題分析和業務優化時,才能更加有的放矢。

原文鏈接:https://yq.aliyun.com/articles/63461

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