本文及后续文章,Rdis版本均是v3.2.8
我们会经常选择使用sortdst数据结构,是由于其提供的操作非常丰富,可以满足非常多的应用场景。sortdst数据结构是由skiplist(跳跃列表)、ziplist和dict实现的。
skiplist本质上是一种查找数据据结构,即根据给定的ky,快速查到它所对应的valu。
skiplist是一种链式数据结构,在外观表现上其具有两个属性:分值和保存的对象。
skiplist通过对每个节点的分值进行排序从而达到排序每个节点的目的。
skiplist为了实现跳跃表的快速修改和查询操作,其内部还在每个节点上都保存了一个用于指向其后节点的指针数组,以及一个指向前一个节点的指针;并且为了快速获取跳跃表的节点数目和指针数组的最大长度,其分别创建了一个lngth和一个lvl属性用于快速查询。
skiplist的平均时间复杂度为O(logN),最坏复杂度为O(N),其性能一般情况下可以与平衡树相媲美。
一、skiplist数据结构定义
我们看下srvr.h中代码
/*ZSETsusaspcializdvrsionofSkiplists*/
typdfstructzskiplistNod{
/*成员objct对象*/
robj*obj;
/*分数字段依赖此值对skiplist进行排序*/
doublscor;
/*插入层中指向上一个元素lvl数组*/
structzskiplistNod*backward;
structzskiplistLvl{
/*每层中指向下一个元素指针*/
structzskiplistNod*forward;
/*距离下一个元素之间元素数量,即forward指向的元素*/
unsigndintspan;
}lvl[];
}zskiplistNod;
typdfstructzskiplist{
/*跳跃表头节点和尾节点*/
structzskiplistNod*hadr,*tail;
/*跳跃表中元素个数*/
unsigndlonglngth;
/*跳跃表当前最大层数*/
intlvl;
}zskiplist;
1、zskiplistNod定义了skiplist的节点结构
obj字段存放的是节点数据,它的类型是一个stringrobj。本来一个stringrobj可能存放的不是sds,而是long型,但zadd命令在将数据插入到skiplist里面之前先进行了解码,所以这里的obj字段里存储的一定是一个sds。这样做的目的应该是为了方便在查找的时候对数据进行字典序的比较,而且,skiplist里的数据部分是数字的可能性也比较小。
scor字段是数据对应的分数。
backward字段是指向链表前一个节点的指针(前向指针)。节点只有1个前向指针,所以只有第1层链表是一个双向链表。
lvl[]存放指向各层链表后一个节点的指针(后向指针)。每层对应1个后向指针,用forward字段表示。另外,每个后向指针还对应了一个span值,它表示当前的指针跨越了多少个节点。span用于计算元素排名(rank),这正是前面我们提到的Rdis对于skiplist所做的一个扩展。需要注意的是,lvl[]是一个柔性数组(flxiblarraymmbr),因此它占用的内存不在zskiplistNod结构里面,而需要插入节点的时候单独为它分配。也正因为如此,skiplist的每个节点所包含的指针数目才是不固定的,我们前面分析过的结论——skiplist每个节点包含的指针数目平均为1/(1-p)——才能有意义。
2、zskiplist定义了真正的skiplist结构,它包含:
头指针hadr和尾指针tail。
链表长度lngth,即链表包含的节点总数。注意,新创建的skiplist包含一个空的头指针,这个头指针不包含在lngth计数中。
lvl表示skiplist的总层数,即所有节点层数的最大值。
总结下跳跃表主要有以下几个部分构成:
1、表头had:负责维护跳跃表的节点指针
2、节点nod:实际保存元素值,每个节点有一层或多层
3、层lvl:保存着指向该层下一个节点的指针
4、表尾tail:全部由null组成
跳跃表的遍历总是从高层开始,然后随着元素值范围的缩小,慢慢降低到低层。
二、skiplist基本操作
我们先来看下使用zskiplist保存数据的示例:
这里需要说明的是,跳跃表的节点数组的长度是随机的,其值为1到32之间的一个整数。
创建的操作
我们看下t_zsst.c的代码
/*创建一个跳跃表节点*/
zskiplistNod*zslCratNod(intlvl,doublscor,robj*obj){
zskiplistNod*zn=zmalloc(sizof(*zn)+lvl*sizof(structzskiplistLvl));
zn-scor=scor;
zn-obj=obj;
rturnzn;
}
zskiplist*zslCrat(void){
intj;
zskiplist*zsl;
zsl=zmalloc(sizof(*zsl));
zsl-lvl=1;
zsl-lngth=0;
/*初始化创建一个头节点,初始化节点信息*/
zsl-hadr=zslCratNod(ZSKIPLIST_MAXLEVEL,0,NULL);
for(j=0;jZSKIPLIST_MAXLEVEL;j++){
zsl-hadr-lvl[j].forward=NULL;
zsl-hadr-lvl[j].span=0;
}
zsl-hadr-backward=NULL;
zsl-tail=NULL;
rturnzsl;
}
从以上代码中看到,创建跳跃表过程比较简单,初始化zskiplist数据结构,跳跃表默认最大层数32层,跳跃表是按scor进行升序排列.
插入元素的操作
zskiplistNod*zslInsrt(zskiplist*zsl,doublscor,robj*obj){
zskiplistNod*updat[ZSKIPLIST_MAXLEVEL],*x;
unsigndintrank[ZSKIPLIST_MAXLEVEL];
inti,lvl;
rdisAssrt(!isnan(scor));
//在各个层查找节点的插入位置,从头节点开始搜索,一层一层向下搜索,直到直到最后一层,updat数组中保存着每层应该插入的位置
//T_wrost=O(N^2),T_avg=O(NlogN)
x=zsl-hadr;
for(i=zsl-lvl-1;i=0;i--){
/*storrankthatiscrossdtorachthinsrtposition*/
//如果i不是zsl-lvl-1层
//那么i层的起始rank值为i+1层的rank值
//各个层的rank值一层层累积
//最终rank[0]的值加一就是新节点的前置节点的排位
//rank[0]会在后面成为计算span值和rank值的基础
rank[i]=i==(zsl-lvl-1)?0:rank[i+1];
//沿着前进指针遍历跳跃表
//T_wrost=O(N^2),T_avg=O(NlogN)
whil(x-lvl[i].forward
(x-lvl[i].forward-scorscor
//比对分值
(x-lvl[i].forward-scor==scor
//比对成员