您的位置:首页 > 理论基础 > 数据结构算法

Redis内部数据结构实现解析

2015-10-15 13:48 531 查看
Redis目前在Key-Value存储以及缓存系统中有着非常广泛的应用,且以高效快速著称。不同于其他Key-Value数据库,Redis提供了丰富的数据结构类型,value可以是字符串、列表、哈希和有序集等,为用户操作带来了极大的便利。本文希望通过分析其内部数据结构及算法的实现机制,来揭示其高性能的背后的原因。

动态字符串

由于所有的Key都是字符串,字符串在Redis中的应用非常广泛。Redis底层使用sds(Simple Dynamic String)表示简单字符串,从而替代C语言中的char
*类型,以支持高效的字符串扩展操作。

sds的实现方式如下,其中sds是char *的别名。当新增一个value为字符串类型的键值对时,Redis将value存储在一个adshdr实例中,buf[]存储value的实际数据,为buf[]分配的空间长度与value当前所占空间相同,即len为value的长度(包括字符串末尾的’\0’),free为0。在创建adshdr实例完成后,函数将指向buf[]的指针返回给sds,以便于管理adshdr结构体实例的增、删、改、查等操作。

<p><pre name="code" class="cpp">typedef char *sds;
struct sdshdr {
int len; 		// buf 已使用长度
int free; 		// buf 可用长度
char buf[];	  	// 所保存的字符串数据
};





当对value进行append操作时,显然,最初创建时分配给sds的空间已无法满足存储要求,因而Redis会为buf[]动态分配存储空间。当新字符串的总长度小于时sds最大预分配长度时,新的存储空间所能存放的字符串长度为(oldStr.length
+ appendStr.length) * 2 + 1,其中后面的“+1”用来存放‘\0’;否则,就分配appendStr的长度 + 最大预分配长度 的空间。同时,还要同步更新实例中len和free的值,如果buf[]的地址发生了改变,也要更新sds的值。

由于sds中预分配机制的存在,避免了每次value的追加都要进行内存分配步骤,很大地提高了append操作的效率。除此之外,相比于C的char
*,sds使用len变量记录字符已使用的长度,从而使用户可以快速地获取字符串的长度信息。

字典

Redis的字典(Dictionary)的底层实现利用的是hash表。字典的结构体如下:

typedef struct dict {
dictType *type;		// hash表的类型,可以是string, list等
void *privdata; 		// 类型处理函数的私有数据
dictht ht[2]; 		// 哈希表(2 个)
int rehashidx; 		// 是否在进行rehash 的标志位
int iterators; 		// 当前运行的安全迭代器数量
} dict;


哈希表的结构如下:

typedef struct dictht {
dictEntry **table; 		// 哈希表节点指针数组(即bucket)
unsigned long size; 	// 指针数组的大小
// 指针数组的长度掩码,用于计算索引值
unsigned long sizemask;
unsigned long used; 	// 哈希表现有的节点数量
} dictht;




哈希表中的每一个元素指向一个表结点,即dictEntry结构体实例的指针,每个dictEntry由一个键值对和一个指向后继表节点的指针next构成,从而可以使用链地址法处理哈希冲突。



(图来自《Redis内存存储结构分析》)

一个字典实例中有两个hash表,当在空字典中添加第一条数据时,Redis会为ht[0]所指向的table分配一定的存储空间;随着数据量的逐渐增多,字典的索引利用率达到一定比例时,需要对字典进行rehash的操作,从而保证字典的查询和插入效率保持在较高的水准,即字典的expend。这时,会创建一个更大的table(空间至少为ht[0]的两倍),其地址被赋给ht[1],字典中的数据将由ht[0]迁移到ht[1]。

在迁移过程开始时,rehashidx值设置为0,标志着字典处于迁移状态;迁移结束后,ht[0]的数据将被清空,并将ht[1]更改为新的ht[0],ht[1]指向一个新的空hash表,rehashidx的值重新设为-1,标志着字典不在数据迁移状态中。

如果table中的数据量比较大,就会出现迁移时间过长的问题,致使在一个相对较长的时间段内,用户无法进行其他操作。因此在实际中,数据迁移是分阶段完成的,rehashidx会记录rehash进行到ht[0]的哪个索引位置上。在迁移过程中,若对字典进行添加操作,则新的节点将会添加到ht[1]中;若对字典进行删除或查找操作,则需要在ht[0]和ht[1]上同时进行。

由于hash表中数据量是动态变化的,当索引的利用率较低时,也可以通过rehash进行字典收缩(shrink)操作,即用一个将数据迁移到一个更小的table中。不同的是,字典的expend操作是自动触发的,而shrink是通过程序调用执行的。

跳跃表

跳跃表是一种随机化数据结构,基于并联的列表,其插入、查找、删除的时间复杂度均为log(n),可与二叉树相媲美。跳跃表主要由表头、节点和表尾组成,其节点分布在多层链表中。

跳跃表由结构体zskiplist定义:

typedef struct zskiplist {
struct zskiplistNode *header, *tail;	// 头节点,尾节点
unsigned long length; 				// 节点数量
int level; 							// 表中节点的最大层数
} zskiplist;


表的节点由zskiplistNode定义,每个节点中存有一个Score-Member指针对,score为跳跃表的索引,*obj指向存储值member的域。

typedef struct zskiplistNode {
robj *obj;		// member 对象
double score;
struct zskiplistNode *backward; 	// 后退指针
// 层
struct zskiplistLevel {
struct zskiplistNode *forward; 	// 前进指针
unsigned int span; 			// 本层两个节点的间隔
} level[];
} zskiplistNode;


跳跃表在Redis中应用的结构示意图如下:



(图来自 《Redis设计与实现》)

跳跃表在Redis中主要用作有序集类型的底层数据结构,通过层层遍历查找与给定score值相一致的节点,从而获取member的值。跳跃表编码的有序集由一个dictionary和skiplist构成。首先通过dictionary查找出score的值,然后再根据score值在skiplist查取想要获得的value。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: