redis一共支持5种数组类型,这五种数据类型底层是靠一些数据结构来支撑的,或许有些同学认为,我只要会用好这五种数据类型就ok,完全没必要知道其底层的数据结构到底是什么样子的。这种想法应该说一定程度上是对的,当你仅仅用redis来支撑一些小的项目或仅用于测试,几乎不会与这些底层的数据结构打交道。然而,当我们的项目对性能要求比较高,遇到性能瓶颈时需要对性能进行优化,这时候,了解这些数据结构就非常有必要,因为redis性能优化很有可能要与这些数据结构打交道,如果不懂底层数据结构,根本无从下手。让我们一起来看看这些底层的数据结构吧。
#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 */
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 |
字典是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是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<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;
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也是使用sds来存储字符串,只不过REDIS_ENCODING_EMBSTR使用的sds和redisobject的地址空间是连续的,REDIS_ENCODING_EMBSTR只需要分配一次内存,而REDIS_ENCODING_RAW需要分配两次内存。
REDIS_ENCODING_EMBSTR未曾提供字符串修改的方式,如果修改字符串,内部是先转换成REDIS_ENCODING_RAW,然后再进行修改。当键值内容不超过39字节默认使用此编码方式。
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
ZIPLIST 是一种紧缩型的编码方式,用时间换取空间。他牺牲了部分读取性能换取极高的空间利用率。
zlbyte | zltail | zllen | zlend |
---|---|---|---|
表示zl的大小,32位 | zl尾元素的偏移,32位 | zll元素的个数,16位 | 单字节的标示,标示结构体末尾,永远为255 |
ziplist之所以被称为紧凑形的存储,在于元素的大小是更具键和值的内容产生变化。这种存储结构,一但元素的值发生变化,需要先定位到其值的位置,将其删除,然后再把新值插入,可想而知,这种操作要来回移动后面的元素,当key很多的情况下,性能是很低下的。因此,不宜将条目的门限和键值的门限设置的过大。
前一个元素的大小 | 元素的编码类型 | 当前元素的大小 | 当前元素的内容 |
---|---|---|---|
当元素的大小小于254字节时占用1个字节;当元素大小大于254字节时占用5字节 | 2位 | 当元素大小小于63字节,6位;当元素大于63字节小于16383,14位;否则,38位 | 如果是字符串,有几个字符占用几个字节;如果是数字,可能是16位,也有能是32位,视具体数字大小而定 |
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使用哈希表和跳跃表两种数据结构来存储数据,其中哈希表存储元素数值与元素分数的映射关系。跳跃表存储分数到元素的值的映射以实现排序功能。