Redis学习笔记(4)-redis底层数据结构

redis一共支持5种数组类型,这五种数据类型底层是靠一些数据结构来支撑的,或许有些同学认为,我只要会用好这五种数据类型就ok,完全没必要知道其底层的数据结构到底是什么样子的。这种想法应该说一定程度上是对的,当你仅仅用redis来支撑一些小的项目或仅用于测试,几乎不会与这些底层的数据结构打交道。然而,当我们的项目对性能要求比较高,遇到性能瓶颈时需要对性能进行优化,这时候,了解这些数据结构就非常有必要,因为redis性能优化很有可能要与这些数据结构打交道,如果不懂底层数据结构,根本无从下手。让我们一起来看看这些底层的数据结构吧。

1.源代码所定义的8种数据结构

#define REDIS_ENCODING_RAW 0        /* Raw representation */  
#define REDIS_ENCODING_INT 1        /* Encoded as integer */  
#define REDIS_ENCODING_HT 2         /* Encoded as hash table */  
#define REDIS_ENCODING_ZIPMAP 3     /* Encoded as zipmap */  
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */  
#define REDIS_ENCODING_ZIPLIST 5    /* Encoded as ziplist */  
#define REDIS_ENCODING_INTSET 6     /* Encoded as intset */  
#define REDIS_ENCODING_SKIPLIST 7   /* Encoded as skiplist */ 
#defineREDIS_ENCODING_EMBSTR 8      /* Embedded sdsstring encoding */

2.五大数据类型对数据结构的使用

RedisObject 数据结构之一 数据结构之二 数据结构之三
字符串 REDIS_ENCODING_RAW REDIS_ENCODING_EMBSTR REDIS_ENCODING_INT
哈希 REDIS_ENCODING_HT REDIS_ENCODING_ZIPLIST
列表 REDIS_ENCODING_LINKEDLIST REDIS_ENCODING_ZIPLIST
集合 REDIS_ENCODING_HT REDIS_ENCODING_INTSET
有序结合 REDIS_ENCODING_SKIPLIST REDIS_ENCODING_ZIPLIST

3.各个数据类型对数据结构选用详解

字典

字典是redis的最基础的数据结构,redis所有的数据都是通过字典来存储的,一个字典即一个redis的DB,一个redisDB对应一个字典。字典默认使用哈希的方式存储,对于键的哈希码对HashTable取余得到的结果一样的条目,redis采用“链地址发解决冲突”。“链地址法“有一个问题,当所有的条目都在一条链上时,或者条目在HashTable上的集中程度较高,会对查询性能造成影响。redis的采用两个字典的原因是,当第一个字典中数据的碰撞程度较为激烈,会在第二个字典上分配一个更大的HashTable,然后将第一个字典上的数据迁移到第二个字典上。数据迁移是使用的渐进式hash,当ht[0]往ht[1]中迁移数据时,首先在ht[1]中分配内存空间,然后搞一个rehashidx计数器变量,每次有CRUD操作时,迁移一个数据,rehashidx加一,当全部迁移完毕将rehashidx设置为-1,便是rehash结束,这种分而治之的思想可以将运算分散到不同时间进行,避免了集中计算对redis造成的性能瓶颈。

字典上的条目的数据结构如下定义:

struct dictEntry {  
    void *key;  
    union {  
        void *val;  
        uint64_t u64;  
        int64_t s64;  
    } v;  
    struct dictEntry *next;  
} dictEntry;

RedisObject

RedisObject是redis的值的数据结构,也是其最重要的结构。其中,type是指redis定义的五大数据类型:string,hash,list,set,sorted set。encoding是值其编码方式,包括raw,int,embstr,linkedlist,ziplist,ht,intset,skiplist。ptr指向实际的数据结构,不同的编码方式对应不用的数据结构,下面我们将从数据类型的角度,介绍其可以使用的数据结构。

/* The actualRedis Object */
#define REDIS_LRU_BITS 24
#define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1)/* Max value of obj->lru */
#define REDIS_LRU_CLOCK_RESOLUTION 1000/* LRU clock resolution in ms */
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:REDIS_LRU_BITS; /* lru time(relative to server.lruclock) */
    intrefcount;
    void *ptr;
} robj;

4.数据结构详解

字符串使用的数据结构

REDIS_ENCODING_RAW

REDIS_ENCODING_RAW 即原生态的存储结构,就是以字符串形式存储,字符串类型在redis中用sds(simple dynamic string)封装,主要为了解决长度计算和追加效率的问题。sds的结构体如下:

struct sdshdr {  
    // buf 中已占用空间的长度  
    int len;  
    // buf 中剩余可用空间的长度  
    int free;  
    // 数据空间  
    char buf[];  
};  

其中len表示字符串的长度,free用于表示buf中的可用空间,buf是真正存储字符串的地方。redis使用sds来存储字符串,可以防止由于字符串频繁变更导致的内存的重新分配的低效;同时,在查询字符串长度的只要读一个len就能搞定。传统的C语言定义字符创都是以’\0‘作为字符串的结束,要计算字符创的长度每次都需要遍历字符串。同时,每次修改字符串的时候,都要重新分配内存。sds的方式存储字符串明显效率更深一筹。

REDIS_ENCODING_EMBSTR

REDIS_ENCODING_EMBSTR也是使用sds来存储字符串,只不过REDIS_ENCODING_EMBSTR使用的sds和redisobject的地址空间是连续的,REDIS_ENCODING_EMBSTR只需要分配一次内存,而REDIS_ENCODING_RAW需要分配两次内存。
REDIS_ENCODING_EMBSTR未曾提供字符串修改的方式,如果修改字符串,内部是先转换成REDIS_ENCODING_RAW,然后再进行修改。当键值内容不超过39字节默认使用此编码方式。

REDIS_ENCODING_INT

REDIS_ENCODING_INT 存储整数,以long型存储。如过字符串的内容可以用一个64位的有符号整数表示时,redis会将键值转换成long来存储,这样就可以节省使用空间了。用redisObject中的ptr来存储就可以,不需要再分配sdshdr结构。

哈希使用的数据结构

哈希的内部编码类型可能是REDIS_ENCODING_HT或者REDIS_ENCODING_ZIPLIST。在配置文件方式可以指定REDIS_ENCODING_ZIPLIST的使用时机:
即哈希中的条目不大于512和键值的长度不大于64字节就使用ZIPLIST,否则使用HT。

hash-max-ziplist-enties 512
hash-max-ziplist-value 64
REDIS_ENCODING_ZIPLIST

ZIPLIST 是一种紧缩型的编码方式,用时间换取空间。他牺牲了部分读取性能换取极高的空间利用率。

zlbyte zltail zllen zlend
表示zl的大小,32位 zl尾元素的偏移,32位 zll元素的个数,16位 单字节的标示,标示结构体末尾,永远为255

Redis学习笔记(4)-redis底层数据结构
ziplist之所以被称为紧凑形的存储,在于元素的大小是更具键和值的内容产生变化。这种存储结构,一但元素的值发生变化,需要先定位到其值的位置,将其删除,然后再把新值插入,可想而知,这种操作要来回移动后面的元素,当key很多的情况下,性能是很低下的。因此,不宜将条目的门限和键值的门限设置的过大。

前一个元素的大小 元素的编码类型 当前元素的大小 当前元素的内容
当元素的大小小于254字节时占用1个字节;当元素大小大于254字节时占用5字节 2位 当元素大小小于63字节,6位;当元素大于63字节小于16383,14位;否则,38位 如果是字符串,有几个字符占用几个字节;如果是数字,可能是16位,也有能是32位,视具体数字大小而定

列表所用的数据结构

REDIS_ENCODING_LINKEDLIST

REDIS_ENCODING_LINKEDLIST 代表链表,结构类似于标准的双向链表

typedef struct list{
    //表头节点
    listNode  * head;
    //表尾节点
    listNode  * tail;
    //链表长度
    unsigned long len;
    //节点值复制函数
    void *(*dup) (void *ptr);
    //节点值释放函数
    void (*free) (void *ptr);
    //节点值对比函数
    int (*match)(void *ptr, void *key);
}
typedef struct listNode{
      struct listNode *prev;
      struct listNode * next;
      void * value;  
}

集合所用的数据结构

集合可以使用HT或者INTSET来存储该集合,当集合中的元素的个数小于set-max-intset-entries参数指定值(默认512),redis会使用INTSET来存储集合,否则会使用HT来存储集合。
INTSET的定义如下:

typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
}intset

当集合中出现的不是整数或者条目数大于set-max-intset-entries所指定的最大值,则自动转为HT存储。
content中是实际存储的元素,根据encoding不同,每个元素占用的字接大小不同。默认的encoding是INTSET_ENC_INT16,当新增加的元素超过2字节,自动升级为INTSET_ENC_INT16,同样还可以调整为8字节,即INTSET_ENC_INT64。
这种存储方式更加灵活和节约空间,同时元素也是有序的,当时增加或者减少元素时需要移动元素,当元素较多的时候,效率较为低下。

有序结合所用的数据结构

有续集合内部编码方式可以是ZIPLIST或者SKPILIST,同样可以配置如下两项,使用ZIPLIST存储。

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

当编码方式是SKIPLIST的时候,redis使用哈希表和跳跃表两种数据结构来存储数据,其中哈希表存储元素数值与元素分数的映射关系。跳跃表存储分数到元素的值的映射以实现排序功能。