Redis设计与实现-数据结构篇(4)--跳跃表

Redis跳表

Redis中的跳表实现在3.0版本是在Redis.h中,到3.2版本时定义到了server.h中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* ZSETs use a specialized version of Skiplists */
/*
* 跳跃表节点
*/
typedef struct zskiplistNode {
// 成员对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
/*
* 跳跃表
*/
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量,除去第一个节点
unsigned long length;
// 表中层数最大的节点的层数,除去第一个节点
int level;
} zskiplist;

-w859

幂次定律

在Redis中,跳表返回的随机层数值使用的算法就是幂次定律。

  • 含义是:如果某件事的发生频率和它的某个属性成幂关系,那么这个频率就可以称之为符合幂次定律。
  • 表现是:少数几个事件的发生频率占了整个发生频率的大部分,而其余的大多数事件只占整个发生频率的一个小部分。
    1
    2
    3
    4
    5
    6
    int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))//ZSKIPLIST_P(0.25)所以level+1的概率为0.25
    level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
    }

创建跳跃表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
* 创建并返回一个新的跳跃表
*
* T = O(1)
*/
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;

// 分配空间
zsl = zmalloc(sizeof(*zsl));

// 设置高度和起始层数
zsl->level = 1;
zsl->length = 0;

// 初始化表头节点
// T = O(1)
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/*
* 创建一个成员为 obj ,分值为 score 的新节点,
* 并将这个新节点插入到跳跃表 zsl 中。
*
* 函数的返回值为新节点。
*
* T_wrost = O(N^2), T_avg = O(N log N)
*/
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;

redisAssert(!isnan(score));

// 在各个层查找节点的插入位置
// T_wrost = O(N^2), T_avg = O(N log N)
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {

/* store rank that is crossed to reach the insert position */
// 如果 i 不是 zsl->level-1 层
// 那么 i 层的起始 rank 值为 i+1 层的 rank 值
// 各个层的 rank 值一层层累积
// 最终 rank[0] 的值加一就是新节点的前置节点的排位
// rank[0] 会在后面成为计算 span 值和 rank 值的基础
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];

// 沿着前进指针遍历跳跃表
// T_wrost = O(N^2), T_avg = O(N log N)
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
// 比对分值
(x->level[i].forward->score == score &&
// 比对成员, T = O(N)
compareStringObjects(x->level[i].forward->obj,obj) < 0))) {

// 记录沿途跨越了多少个节点
rank[i] += x->level[i].span;

// 移动至下一指针
x = x->level[i].forward;
}
// 记录将要和新节点相连接的节点
update[i] = x;
}

/* we assume the key is not already inside, since we allow duplicated
* scores, and the re-insertion of score and redis object should never
* happen since the caller of zslInsert() should test in the hash table
* if the element is already inside or not.
*
* zslInsert() 的调用者会确保同分值且同成员的元素不会出现,
* 所以这里不需要进一步进行检查,可以直接创建新元素。
*/

// 获取一个随机值作为新节点的层数
// T = O(N)
level = zslRandomLevel();

// 如果新节点的层数比表中其他节点的层数都要大
// 那么初始化表头节点中未使用的层,并将它们记录到 update 数组中
// 将来也指向新节点
if (level > zsl->level) {

// 初始化未使用层
// T = O(1)
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,obj);

// 将前面记录的指针指向新节点,并做相应的设置
// T = O(1)
for (i = 0; i < level; i++) {

// 设置新节点的 forward 指针
x->level[i].forward = update[i]->level[i].forward;

// 将沿途记录的各个节点的 forward 指针指向新节点
update[i]->level[i].forward = x;

/* update span covered by update[i] as x is inserted here */
// 计算新节点跨越的节点数量
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);

// 更新新节点插入之后,沿途节点的 span 值
// 其中的 +1 计算的是新节点
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}

/* increment span for untouched levels */
// 未接触的节点的 span 值也需要增一,这些节点直接从表头指向新节点
// T = O(1)
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;
}

删除节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank 
*
* 内部删除函数,
* 被 zslDelete 、 zslDeleteRangeByScore 和 zslDeleteByRank 等函数调用。
*
* T = O(1)
*/
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
int i;

// 更新所有和被删除节点 x 有关的节点的指针,解除它们之间的关系
// T = O(1)
for (i = 0; i < zsl->level; i++) {
if (update[i]->level[i].forward == x) {
update[i]->level[i].span += x->level[i].span - 1;
update[i]->level[i].forward = x->level[i].forward;
} else {
update[i]->level[i].span -= 1;
}
}

// 更新被删除节点 x 的前进和后退指针
if (x->level[0].forward) {
x->level[0].forward->backward = x->backward;
} else {
zsl->tail = x->backward;
}

// 更新跳跃表最大层数(只在被删除节点是跳跃表中最高的节点时才执行)
// T = O(1)
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--;

// 跳跃表节点计数器减一
zsl->length--;
}

获取节点排名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* Find the rank for an element by both score and key.
*
* 查找包含给定分值和成员对象的节点在跳跃表中的排位。
*
* Returns 0 when the element cannot be found, rank otherwise.
*
* 如果没有包含给定分值和成员对象的节点,返回 0 ,否则返回排位。
*
* Note that the rank is 1-based due to the span of zsl->header to the
* first element.
*
* 注意,因为跳跃表的表头也被计算在内,所以返回的排位以 1 为起始值。
*
* T_wrost = O(N), T_avg = O(log N)
*/
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o) {
zskiplistNode *x;
unsigned long rank = 0;
int i;

// 遍历整个跳跃表
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {

// 遍历节点并对比元素
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
// 比对分值
(x->level[i].forward->score == score &&
// 比对成员对象
compareStringObjects(x->level[i].forward->obj,o) <= 0))) {

// 累积跨越的节点数量
rank += x->level[i].span;

// 沿着前进指针遍历跳跃表
x = x->level[i].forward;
}

/* x might be equal to zsl->header, so test if obj is non-NULL */
// 必须确保不仅分值相等,而且成员对象也要相等
// T = O(N)
if (x->obj && equalStringObjects(x->obj,o)) {
return rank;
}
}

// 没找到
return 0;
}

区间操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range) { //返回第一个分数在range范围内的节点
zskiplistNode *x;
int i;

/* If everything is out of range, return early. */
if (!zslIsInRange(zsl,range)) return NULL; //如果不在范围内,则返回NULL,确保至少有一个节点符号range

//判断下限
x = zsl->header;//遍历跳跃表
for (i = zsl->level-1; i >= 0; i--) {//遍历每一层
/* Go forward while *OUT* of range. */
while (x->level[i].forward && //如果该层有下一个节点且
!zslValueGteMin(x->level[i].forward->score,range))//当前节点的score还小于(小于等于)range的min
x = x->level[i].forward; //继续指向下一个节点
}

/* This is an inner range, so the next node cannot be NULL. */
x = x->level[0].forward; //找到目标节点
redisAssert(x != NULL); //保证能找到

/* Check if score <= max. */
//判断上限
if (!zslValueLteMax(x->score,range)) return NULL; //该节点的分值如果比max还要大,就返回NULL
return x;
}

zskiplistNode *zslLastInRange(zskiplist *zsl, zrangespec *range) {//返回最后一个分数在range范围内的节点
zskiplistNode *x;
int i;

/* If everything is out of range, return early. */
if (!zslIsInRange(zsl,range)) return NULL; //如果不在范围内,则返回NULL,确保至少有一个节点符号range

//判断上限
x = zsl->header;//遍历跳跃表
for (i = zsl->level-1; i >= 0; i--) { //遍历每一层
/* Go forward while *IN* range. */
while (x->level[i].forward && //如果该层有下一个节点且
zslValueLteMax(x->level[i].forward->score,range))//当前节点的score小于(小于等于)max
x = x->level[i].forward; //继续指向下一个节点
}

/* This is an inner range, so this node cannot be NULL. */
redisAssert(x != NULL);//保证能找到

/* Check if score >= min. */
//判断下限
if (!zslValueGteMin(x->score,range)) return NULL; //如果找到的节点的分值比range的min还要小
return x;
}

为什么Redis要使用跳表来实现有序集合而不是红黑树?

Redis中有序集合支持的核心操作主要有:

  • 插入一个数据
  • 删除一个数据
  • 查找一个数据
  • 按照区间查找数据
  • 迭代输出有序序列

其中插入、删除、查找以及迭代有序序列这几个操作,红黑树也能够完成,时间复杂度跟跳表是一样的。

但是按照区间查找数据这个操作,红黑树的效率没有跳表高。跳表可以在O(logn)时间复杂度定位区间的起点,然后在原始链表中顺序先后查询,这非常高效。

此外,相比于红黑树,跳表还具有代码更容易实现、可读性高、不容易出错、更加灵活等特点。