本节将对Redis底层的六种数据结构展开详述:简单动态字符串、链表、字典、跳跃表、整数集合、压缩列表。
一、简单动态字符串(SDS)
Redis基于C语言开发但并没有直接使用C语言传统的字符串,而是构建一种叫简单动态字符串(simple dynamic string,SDS)的抽象类型作为Redis默认的字符串表示。SDS不仅用来保存数据库中的字符串值,同时还用于实现缓冲区(buffer)。
在Redis源码sds.h/sdshdr中可以看到SDS的结构体定义如下:
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
上图展示了一个SDS,len属性里保存了buf中已占用的空间长度,free属性里面保存了buf中剩余可用的空间长度,buf属性是一个char类型的数组保存具体的字符串(注意:空字符串'\0'不算在len和buf中)。 相比于C字符串,Redis构建的SDS具有以下优点:
1、常数复杂度获取字符串的长度:len属性保存了字符串的长度;
2、杜绝缓冲区溢出:当对SDS进行修改时,先检查SDS的空间是否满足修改所需要的空间要求,如果不满足,则先扩展空间,然后执行修改操作;
3、减少修改字符串带来的内存重分配次数:
(1)空间预分配:当SDS的长度小于1MB时,分配(2 * len + 1B)的空间;当SDS的长度大于等于1MB时,分配(len + 1MB + 1B)的空间;
(2)惰性空间释放:当缩短字符串时,并不会立即使用内存重分配来回收多出来的字符,而是记录在free属性中;
二、链表
链表提供了高效的节点重排、顺序访问、灵活的增删节点的能力。因此,在Redis的许多地方都应用到链表。
在Redis的源码adlist.h/listNode中可以看到链表的结构体定义如下:
/*
* 双端链表节点
*/
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
然后多个listNode结构组成链表,然后使用adlist.h/list来持有链表:
/*
* 双端链表结构
*/
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;
三、字典
字典用于保存键值对,可以方便的根据key值操作对应的value值。Redis数据库就是使用字典作为底层实现的,实现对数据库的增、删、改、查等操作。
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点保存了具体的在键值对。Redis的源码dict.h/dict可以看到字典的结构体定义如下:
/*
* 字典
*/
typedef struct dict {
// 类型特定函数,Redis为不同用途的字典设置不同的类型特定函数
dictType *type;
// 私有数据,传递给特定类型函数的可选参数
void *privdata;
// 哈希表,一般情况下字典使用ht[0],ht[1]只会在对ht[0]进行rehash时使用
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
哈希表的实现定义在dict.h/dictht中:
/*
* 哈希表
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
// 哈希表数组,存放具体的键值对
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
针对哈希表的每个节点使用dictEntry表示,dictEntry中保存键值对:
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
当将一个新的键值对添加到字典中时,程序先根据键值对的键计算出哈希值和索引值,然后再根据索引值将新键值对的哈希表节点存放到哈希表数组的指定索引上面。Redis计算哈希值和索引值的方法如下:
// 使用字典设置的哈希函数,计算键key的哈希值
hash = dict->type->hashFunction(key)
// 使用哈希表的sizemask属性和哈希值,计算出索引值
// 如果不是rehash状态,则使用ht[0]
index = hash & dict->ht[x].sizemask;
使用哈希表时,会遇到哈希冲突问题,Redis采用的链接地址法(拉链法)解决哈希冲突;随着操作的进行,哈希表中的键值对会逐渐增加或减少,为了让负载因子维持在一个合理的范围内,需要对哈希表的大小进行相应的扩展或收缩,在Redis中通过rehash完成,步骤如下:
这个rehash动作并不是一次性、集中式的完成,而是分多次、渐进式地完成,以避免在数据量很大时导致计算量过大导致服务器在一段时间内停止服务。由于在rehash过程中,字典会同时使用ht[0]和ht[1]两个哈希表,则在执行rehash期间,所有的添加操作都会保存到ht[1]上,而查找、更新、删除等操作都会先在ht[0]中查找,如果没找到再到ht[1]上查找。
四、跳跃表
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点维护多个指向其他节点的指针,从而达到快速访问的目的。跳跃表支持平均O(logN)、最坏O(N)的复杂度查找节点,还可以通过顺序操作来批量处理节点,因此大部分情况下跳跃表的效率可以和平衡树媲美,并且因为跳跃表的实现比平衡树来得更简单,所以有不少程序都使用跳跃表来代替平衡树。
在Redis源码中redis.h/zskiplist可以看到跳跃表的定义:
/*
* 跳跃表
*/
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数,表头结点的层数不计算在内
int level;
} zskiplist;
其中表节点的定义如下:
/*
* 跳跃表节点
*/
typedef struct zskiplistNode {
// 成员对象
robj *obj;
// 分值,跳跃表中节点按各自所保存的分值从小到大排列
double score;
// 后退指针,指向当前节点的前一个节点
struct zskiplistNode *backward;
// 层,每次创建一个新节点,程序按幂次定律随机生成一个1~32的值作为level数组的大小(层高度)
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度,前进指针所指向的节点和当前节点的距离
unsigned int span;
} level[];
} zskiplistNode;
五、整数集合
整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它保存的类型为int16_t、int32_t、int64_t的整数值,并保证集合中元素不会重复出现。
在Redis源码inset.h/inset中可以看到整数集合的定义:
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组,各项在数组中按值大小有序地排序并且不包含重得项
int8_t contents[];
} intset;
由于整数集合有三种类型,当添加比当前数组元素的类型长的元素时,需要对当前集合先升级,然后才能执行添加操作,升级的步骤:
(1)根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间;
(2)将底层数组现有的所有元素都转型为新元素相同的类型,然后放到正确的位置上(大小有序);
(3)将新元素添加到数组中;
升级策略可以提高整数集合的灵活性、亦可节约内存,但Redis的整数集合一旦对数组升级了,就一直保持升级后的状态不支持降级操作。
六、压缩列表
压缩列表(ziplist)是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点,每个结点可以保存一个字节数组或一个整数值。
压缩列表的每个节点可以保存一个字节数组或一个整数值,有三个部分组成:previous_entry_length、encoding、content:
(1)previous_entry_length:以字节为单位,记录当前节点的前一个节点的长度。如果前一个节点长度小于254字节,则该属性的长度为1字节,前一节点的长度就保存在这个字节中;如果前一节点长度大于等于254字节,则该属性的长度为5字节,第1个字节设置为0xFE(十进制254),后面4个字节保存前一节点的长度。
(2)encoding:记录节点的content属性中所保存的数据类型及长度。
(3)content:保存节点的值。
参考文献
1、http://www.redis.net.cn/tutorial/3506.html
2、《Redis设计与实现》第二版---黄健宏
3、https://github.com/xingzhexiaozhu/redis-3.0-annotated
4、http://www.yiibai.com/redis/redis_strings.html