redis數據結構

概述
redis是目前最常用的高效緩存系統,在互聯網行業中使用廣泛;因此打算了解下其內部采用的數據結構;

redisObject

redis使用redisObject表示鍵值;結構如下:

typedef struct redisObject {
    unsigned type:4;//對象類型,可以通過type命令查看
    unsigned encoding:4;//對象編碼,可以通過object encoding命令查看
    unsigned lru:24; //如果采用LRU策略,記錄相對于 server.lruclock的時間,如果采用LFU,低8bit記錄頻率,高16bit記錄時間;可以通過object idletime命令查看
    int refcount;//引用數,如果為0需要釋放內存;可以通過object refcount命令查看
    void *ptr; //指針,指向具體的值
} robj;

其中type表示redis支持的數據類型:

  • String
  • List
  • Set
  • SortedSet
  • Hash

encoding表示對象的編碼格式:

  • ENCODING_RAW
  • ENCODING_INT
  • ENCODING_HT
  • ENCODING_ZIPMAP
  • ENCODING_ZIPLIST
  • ENCODING_INTSET
  • ENCODING_SKIPLIST
  • ENCODING_EMBSTR
  • ENCODING_QUICKLIST

對象類型和編碼方式的對應關系為:

對象類型 編碼方式
String ENCODING_RAW、ENCODING_EMBSTR、ENCODING_INT
List ENCODING_QUICKLIST
Set ENCODING_INTSET、ENCODING_HT
SortedSet ENCODING_SKIPLIST, ENCODING_ZIPLIST
Hash ENCODING_ZIPLIST,ENCODING_HT

根據不同的類型(type),redis會采用不同的結構,通過ptr進行引用;
下面具體分析Redis提供的幾種數據類型:

字符串

當對象為字符串即redisObject的type為String時,encoding可采用ENCODING_RAW、ENCODING_EMBSTR或ENCODING_INT;那么redis是如何確定encoding的呢?

if (len <= 20 && string2l(s,len,&value)) {//如果字符串可以轉換為數字,則使用ENCODING_INT編碼
        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 {//如果設置了maxmemory而且內存過期策略采用LRU或LFU,則不使用共享整數,因為這個時候,要求每個對象有自己的lru信息供LRU或LFU算法使用
            if (o->encoding == OBJ_ENCODING_RAW) sdsfree(o->ptr);
            o->encoding = OBJ_ENCODING_INT;
            o->ptr = (void*) value;
            return o;
        }
    }

   //字符串長度小于44,使用ENCODING_EMBSTR,否則使用RAW
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;

        if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }

可以看到當encoding為ENCODING_INT時,redisObject的ptr指向long;而當采用ENCODING_EMBSTR和ENCODING_RAW時,ptr指向的都是SDS對象;

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /*低位3bit表示SDS_TYPE,其余5bit表示長度 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* buf數組中已使用的字節長度 */
    uint8_t alloc; /* 預分配字節數,不包括'\0' */
    unsigned char flags; /* 低位3bit表示SDS_TYPE,其余5bit未使用*/
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* buf數組中已使用的字節長度 */
    uint16_t alloc; /* 預分配字節數,不包括'\0'*/
    unsigned char flags; /* 低位3bit表示SDS_TYPE,其余5bit未使用 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* buf數組中已使用的字節長度 */
    uint32_t alloc; /* 預分配字節數,不包括'\0' */
    unsigned char flags; /* 低位3bit表示SDS_TYPE,其余5bit未使用*/
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* buf數組中已使用的字節長度 */
    uint64_t alloc; /* 預分配字節數,不包括'\0' */
    unsigned char flags; /* 低位3bit表示SDS_TYPE,其余5bit未使用 */
    char buf[];
};

可以看到,redis為不同長度的字符串定義了不同的結構體;另外為了可以繼續使用標準C的字符串函數,buf以'\0'結尾;

ENCODING_EMBSTR和ENCODING_RAW有什么區別呢?

//兩次分配內存,分別為redisObject和SDS對象分配內存
robj *createRawStringObject(const char *ptr, size_t len) {
    return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}

robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    //一次性申請redisObject和SDS的內存
    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) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

List

當redisObject的type為List時,表示列表對象;根據前面的說明,List的encoding采用ENCODING_QUICKLIST;簡單來說quicklist是由ziplist為節點組成的list;
quicklist結構定義如下:

typedef struct quicklist {
    quicklistNode *head;//頭節點
    quicklistNode *tail;//尾節點
    unsigned long count; //列表元素數目
    unsigned int len; //quicklistNode數目
    int fill : 16;              /* fill factor for individual nodes */
    unsigned int compress : 16; //從尾節點算起,不壓縮節點的數目;
} quicklist;

typedef struct quicklistNode {
    struct quicklistNode *prev;//前節點
    struct quicklistNode *next;//后節點
    unsigned char *zl;// 指向ziplist的指針
    unsigned int sz;             /* ziplist占用字節 */
    unsigned int count : 16;     /* ziplist中的元素數目 */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; //是否被壓縮過
    unsigned int extra : 10; //預留字段,可用于后續擴展
} quicklistNode;

Set

當redisObject的type為Set時,表示Set對象;根據前面的說明,Set的encoding采用ENCODING_INTSET和ENCODING_HT;如果Set中的每個元素都是數字則采用ENCODING_INTSET,否則采用ENCODING_HT編碼;

Set可以采用ENCODING_HT編碼,這個很好理解,就像Java中的HashSet采用HashMap實現一樣;這邊單獨介紹下intset:

typedef struct intset {
    uint32_t encoding;//數組元素編碼,見下文說明
    uint32_t length;//數組元素個數
    int8_t contents[];
} intset;

可以看到intset實際上就是個數組,它要求set中的元素都是數字,而且每個元素占用的內存空間都是固定的,通過encoding來確定:

  • INTSET_ENC_INT16:2個字節,范圍為-32768~32767
  • INTSET_ENC_INT32:4個字節,范圍為-2147483648~2147483647
  • INTSET_ENC_INT64:8個字節,范圍為-9223372036854775808~9223372036854775807

注意:intset中的元素是按照從小到大的順序排列的,不允許出現重復;

有人可能有疑問,為什么數組的類型是int8_t?這是因為如果元素占用兩個字節,則可以用兩個數組元素表示;如果占用4個字節,則用4個數組元素表示;因此contents的長度除以每個元素占用的字節數才是intset的真正長度;另外由于整個intset采用同一編碼,因此即使intset中只有一個元素比較大(8個字節),其它元素都比較小(2個字節),仍然要采用8個字節表示每個元素,這時內存會存在浪費;

SortedSet

SortedSet的encoding采用ENCODING_SKIPLIST或ENCODING_ZIPLIST;
redis.conf文件中有兩個配置項:

  • zset-max-ziplist-entries:當元素數目超出該配置項的值,不能使用ziplist編碼,默認為128;
  • zset-max-ziplist-value:當單個元素占用字節數超出該配置項的值,不能使用ziplist編碼,默認為64;

ziplist編碼

ziplist編碼格式如下:

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
  • zlbytes:4bytes的無符號整數,表示ziplist占用的總字節數(即包括zlbytes和zlend的總字節數);
  • zltail:4bytes的無符號整數,表示最后一個entry相對ziplist起始地址的偏移,通過該字段,redis無需遍歷整個列表即可找到末元素;
  • zllen:2個字節,ziplist包含的entry數量;由于2個字節最多表示65535,因此當元素數目大于65535時,需要遍歷整個列表獲取列表的entry數量;
  • zlend:特殊值0xFF,表示ziplist的結束;

注意:上述字段都采用little endian;

entry的編碼格式如下:

<prevlen> <encoding> <entry-data>
  • prevlen:前一個entry占用的字節數,可用于從末entry節點遍歷ziplist;根據前entry節點長度的不同,占用1或5bytes;如果前entry節點長度小于等于254,prevlen占用1bytes;否則會占用5bytes,并且第一個bytes會被設置為0xFF,后4bytes表示長度;
  • encoding:由于entry-data可能為數字也可能為字符,而且占用的字節數也不一樣,因此通過encoding來區分;

下面具體來看看encoding的定義:

  • |00pppppp|
    1字節,用6bit表示字符串的長度;
  • |01pppppp|qqqqqqqq|
    2字節,用14bit表示字符串的長度,采用big endian;
  • |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt|
    5字節,第一個字節的剩余6bit為0,不使用;剩余4字節表示字符串的長度,采用big endian;
  • |11000000|
    3字節,用剩余2字節表示整數;
  • |11010000|
    5字節,用剩余4字節表示整數;
  • |11100000|
    9字節,用剩余8字節表示整數;
  • |11110000|
    4字節,用剩余3字節表示整數;
  • |11111110|
    2字節,用剩余1字節表示整數;
  • |1111xxxx|
    表示0~12的整數;由于11110000和11111110前面被使用了,因此xxxx表示的是1~13,需要將其減去1得到表示的整數值;

注意:上述整數都采用little endian;

從上述介紹可以看出實際上ziplist是個雙向鏈表,可以從表頭遍歷查找,也可從表尾遍歷查找;

skiplist編碼

當采用skiplist編碼時,redisObject的ptr指向的是zset:

 typedef struct zset {
    dict *dict;//key為sortedset元素,value為score
    zskiplist *zsl;//指向跳躍列表
} zset;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;//頭尾指針
    unsigned long length;//節點數量
    int level;//表中節點的最大層數
} zskiplist;

typedef struct zskiplistNode {
    sds ele;//sortedset元素
    double score;//分值
    struct zskiplistNode *backward;//后退指針,用于從從表尾往表頭訪問節點
    struct zskiplistLevel {
        struct zskiplistNode *forward;//前向指針,用于從表頭往表尾訪問節點
        unsigned int span;//記錄當前節點與前向指針指向節點的跨度,即相距幾個節點;
    } level[];
} zskiplistNode;

可以看到由于存在level,節點查找時,不用逐個節點查找,而是可以跳躍查找,因此查找速度會更快;

Hash

Hash的encoding采用ENCODING_ZIPLIST或ENCODING_HT,關于ziplist,在SortedSet中已經介紹,只不過Hash會用相鄰的兩個entry表示key和value;
下述兩個變量會影響Hash的編碼:

  • hash-max-ziplist-entries:默認為512;如果元素數目大于該值,需要采用ENCODING_HT編碼;
  • hash-max-ziplist-value:默認為64;如果單個元素占用字節數大于該值,需要采用ENCODING_HT編碼;

下面看看ENCODING_HT,關于Hash,可以對比Java中HashMap的實現:

  1. Java中HashMap的底層存儲采用數組,每個數組里面的元素實際上是鏈表的頭節點;往Map里面添加元素時,首先根據key計算出數組下標,然后將節點添加到數組下標指定的鏈表頭部;
  2. redis的Hash實現也類似,但在幾個地方進行了優化:
typedef struct dictht {
    dictEntry **table;//哈希表數組
    unsigned long size;//哈希表大小
    unsigned long sizemask;//哈希表大小掩碼,用于計算索引值,總是等于size-1
    unsigned long used;//該哈希表以有節點數量
} dictht;

typedef struct dict {
    dictType *type;//類型,不同類型dict可以有不同的計算索引哈希函數實現
    void *privdata;
    dictht ht[2];
    long rehashidx; //rehash索引, -1表示不在rehash
    unsigned long iterators;
} dict;

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;//無符號整數,
        int64_t s64;//有符號數
        double d;
    } v;
    struct dictEntry *next;//下個entry指針
} dictEntry;
  • dict中包含兩個ht元素,這兩個ht是用于rehash,所謂的rehash也就是哈希表擴容或縮容;像Java中是同步進行的,而redis中為了提高響應時間,采用的是異步的方式,將元素從ht[0]遷移到ht[1];
  • redis實現了更豐富的哈希函數,例如MurmurHash2;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容