跳跃表(zskiplist)在redis中作为sorted set的底层实现,今天来学习一下。
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {//跳跃表节点
sds ele; // 成员对象[必须唯一]
double score; // 分值[用来排序][可以相同]
struct zskiplistNode *backward; // 后退指针用于从表头向表尾遍历
struct zskiplistLevel { // 层
struct zskiplistNode *forward; // 前进指针
unsigned long span;// 跨度
} level[]; //数组
} zskiplistNode;
typedef struct zskiplist { //跳跃表
struct zskiplistNode *header, *tail; // 指向表头节点和表尾节点
unsigned long length; // 表中节点的数量
int level; // 表中层数最大的节点的层数
} zskiplist;
zskiplist中有头指针和尾指针,尾指针方便从尾部遍历。zskiplistNode中有个结构体zskiplistLevel,这个结构体是跳跃表的实现,level是个结构体数组,其中forward指针有用跨度,可以指向非相邻的结点。span表示forward指向的结点距离本结点多远。
/* Create a new skiplist. */
zskiplist *zslCreate(void) {//创建并返回一个新的跳跃表
int j;
zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl));//跳跃表
zsl->level = 1;
zsl->length = 0;
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
zsl->header->backward = NULL;
zsl->tail = NULL;
return zsl;
}
刚开始默认结点最大层级是1,指针赋空。
int zslRandomLevel(void) {//返回一个随机值,用作新跳跃表节点的层数
int level = 1; //0xFFFF 65535 //random()返回一个[0...1)的随机数
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))//random()&0xFFFF始终小于65535
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
random是一个伪随机数,执行多次之后,返回的level也是一个固定的序列,后面会测试一下。
/* Insert a new node in the skiplist. Assumes the element does not already
* exist (up to the caller to enforce that). The skiplist takes ownership
* of the passed SDS string 'ele'. */
//创建一个成员为 ele ,分值为 score 的新节点,并将这个新节点插入到跳跃表 zsl 中
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) { //mapan
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score));
x = zsl->header; //取出头
for (i = zsl->level-1; i >= 0; i--) {//在各个层查找节点的插入位置
/* store rank that is crossed to reach the insert position */
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
//沿着前进指针遍历跳跃表
//首先比较分值
while (x->level[i].forward &&(x->level[i].forward->score < score || (x->level[i].forward->score == score && sdscmp(x->level[i].forward->ele,ele) < 0) ))
{
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;// 记录将要和新节点相连接的节点
}
/* we assume the element is not already inside, since we allow duplicated
* scores, reinserting the same element should never happen since the
* caller of zslInsert() should test in the hash table if the element is
* already inside or not. */
level = zslRandomLevel();
if (level > zsl->level) {// 如果新节点的层数比表中其他节点的层数都要大
for (i = zsl->level; i < level; i++) {// 初始化未使用层
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = zsl->length;
}
zsl->level = level; // 更新表中节点最大层数
}
x = zslCreateNode(level,score,ele);
// 将前面记录的指针指向新节点,并做相应的设置
for (i = 0; i < level; i++) { //1
x->level[i].forward = update[i]->level[i].forward;// 设置新节点的 forward 指针
update[i]->level[i].forward = x;// 将沿途记录的各个节点的 forward 指针指向新节点
/* update span covered by update[i] as x is inserted here */
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);// 计算新节点跨越的节点数量
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* increment span for untouched levels */
// 未接触的节点的 span 值也需要增一,这些节点直接从表头指向新节点
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
// 设置新节点的后退指针
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward) //设置新建节点前面的后退指针
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++;
return x;
}
跳跃表的插入函数,我们写一个程序测试一下:
int t_zsetTest() //mapan
{
zskiplist *p = zslCreate();
zslInsert(p,1.1,"a");
zslInsert(p,3.3,"b");
zslInsert(p,2.2,"c");
zslInsert(p,4.4,"d");
zslInsert(p,0.9,"e");
zslInsert(p,3.8,"f");
zslInsert(p,5.5,"g");
printf("----------------\n");
zslInsert(p,1.3,"h");
return 0;
}
先看一下,执行对应插入操作所获取的层级:
mapan@mapan-virtual-machine:~/redis-5.0.5/src$ ./redis-server
level=1,score=1.100000
level=2,score=3.300000
level=1,score=2.200000
level=1,score=4.400000
level=1,score=0.900000
level=1,score=3.800000
level=1,score=5.500000
----------------
level=2,score=1.300000
mapan@mapan-virtual-machine:~/redis-5.0.5/src$ ./redis-server
level=1,score=1.100000
level=2,score=3.300000
level=1,score=2.200000
level=1,score=4.400000
level=1,score=0.900000
level=1,score=3.800000
level=1,score=5.500000
----------------
level=2,score=1.300000
你每次运行所对应的层级是一个固定的序列。3.3和1.3对应的层级是2,其他都是1。当执行到zslInsert(p,5.5,"g")时,我们看一下跳跃表的结构。
最下面一层对应都是L1。这时候在执行zslInsert(p,1.3,"h");操作,就会先遍历L2对应的那层,在遍历L1对应的那层。
/* Delete an element with matching score/element from the skiplist.
* The function returns 1 if the node was found and deleted, otherwise
* 0 is returned.
*
* If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise
* it is not freed (but just unlinked) and *node is set to the node pointer,
* so that it is possible for the caller to reuse the node (including the
* referenced SDS string at node->ele). */
//从跳跃表 zsl 中删除包含给定节点 score 并且带有指定对象 ele 的节点
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i;
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) { //逐层遍历,遍历完后要么x指向对应节点,要么找不到
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
x = x->level[i].forward;
}
update[i] = x;// 记录沿途节点
printf("score=%lf,i=%d\n",update[i]->score,i);
}
/* We may have multiple elements with the same score, what we need
* is to find the element with both the right score and object. */
x = x->level[0].forward;
if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
printf("x->score=%lf\n",x->score);
zslDeleteNode(zsl, x, update);
if (!node)
zslFreeNode(x);
else
*node = x; //node用来保存结点,用于重复使用
return 1;
}
return 0; /* not found */
}
删除结点首先也要遍历结点。
/* Update the score of an elmenent inside the sorted set skiplist.
* Note that the element must exist and must match 'score'.
* This function does not update the score in the hash table side, the
* caller should take care of it.
*
* Note that this function attempts to just update the node, in case after
* the score update, the node would be exactly at the same position.
* Otherwise the skiplist is modified by removing and re-adding a new
* element, which is more costly.
*
* The function returns the updated element skiplist node pointer. */
//更新排序后的跳跃表中元素的score。
zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i;
/* We need to seek to element to update to start: this is useful anyway,
* we'll have to update or remove it. */
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward &&
(x->level[i].forward->score < curscore ||
(x->level[i].forward->score == curscore &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
x = x->level[i].forward;
}
update[i] = x;
}
/* Jump to our element: note that this function assumes that the
* element with the matching score exists. */
x = x->level[0].forward;
serverAssert(x && curscore == x->score && sdscmp(x->ele,ele) == 0); //确保找到对应的结点
printf("assert\n");
/* If the node, after the score update, would be still exactly
* at the same position, we can just update the score without
* actually removing and re-inserting the element in the skiplist. */
if ((x->backward == NULL || x->backward->score < newscore) &&
(x->level[0].forward == NULL || x->level[0].forward->score > newscore)) //newscore修正后,要注意集合的顺序
{
x->score = newscore;
return x;
}
/* No way to reuse the old node: we need to remove and insert a new
* one at a different place. */
zslDeleteNode(zsl, x, update);
zskiplistNode *newnode = zslInsert(zsl,newscore,x->ele);
/* We reused the old node x->ele SDS string, free the node now
* since zslInsert created a new one. */
x->ele = NULL;
zslFreeNode(x);
return newnode;
}
更新跳跃表中的元素时,要保证更新后还是有序的,这就可能需要删除原来的阶段,重新插入新的结点。