什么是跳表
跳表(Skip List),首先它是链表,是一种随机化的数据结构,Redis 使用跳表作为有序集合(Sorted Set)的底层实现之一。跳表能够提供高效的插入、删除、查找操作。本文通过阅读源码来分析跳表的工作原理。
跳表的设计思想
跳表的设计思想是通过多级链表来加速查找操作。在一个简单的链表中,查找元素的时间复杂度是 O(n),而在跳表中,通过引入多级索引,查找操作的平均时间复杂度可以降到 O(log n)。
跳表的基本结构如下图所示:
Level 4: 1---------------------->7
Level 3: 1-------->4------------>7
Level 2: 1-------->4------>6---->7
Level 1: 0---->1---->2-->4-->5-->6-->7-->8
每个节点以一定的概率提升到更高一级,形成多个层级的链表。最高层级的链表包含所有节点的索引,而底层链表包含所有节点。
跳表的结构
在 Redis 的源码中,跳表的数据结构定义在 server.h
文件中,主要由以下几个结构体组成:
zskiplistNode
:跳表节点。zskiplist
:跳表。
// 跳表节点
typedef struct zskiplistNode {
double score; // 节点的分数,排序、查找使用
sds ele; // 节点的值
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; // 当前最大层级,默认1
} zskiplist;
跳表的操作
接下来,我们来看一下 Redis 中对跳表的主要操作:插入、删除和查找。具体实现代码在 t_zset.c
文件中:
创建跳表节点
创建一个新的跳表节点:
zskiplistNode* zslCreateNode(int level, double score, sds ele) {
// 为节点分配内存,level 决定节点具有的层数
zskiplistNode *zn = zmalloc(sizeof(*zn) + level * sizeof(struct zskiplistLevel));
zn->score = score; // 节点的分数
zn->ele = ele; // 节点的值
return zn;
}
创建跳表
创建一个新的跳表:
zskiplist* zslCreate(void) {
int j;
zskiplist *zsl;
// 分配跳表的内存
zsl = zmalloc(sizeof(*zsl));
zsl->level = 1; // 初始层级为 1
zsl->length = 0; // 初始长度为 0
// 创建头节点,最大层数为 ZSKIPLIST_MAXLEVEL
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;
}
插入
插入操作的核心在于找到新节点插入的位置,并更新相关的指针:
zskiplistNode* zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
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 &&
sdscmp(x->level[i].forward->ele, ele) < 0))) { // 查找插入位置
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x; // 记录每层的前驱节点
}
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 = zmalloc(sizeof(*x)+level*sizeof(struct zskiplistLevel)); // 分配新节点内存
x->score = score;
x->ele = sdsdup(ele);
for (i = 0; i < level; i++) { // 插入新节点,并更新相关指针和跨度
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
x->backward = (update[0] == zsl->header) ? NULL : update[0]; // 更新 backward 指针
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x; // 如果新节点是最后一个节点,更新尾节点指针
zsl->length++; // 更新跳表的长度
return x;
}
这段代码做了以下几个关键步骤:
- 遍历各层,找到新节点的插入位置。
- 随机确定新节点的层数,并更新跳表的层数。
- 插入新节点,并更新相关指针和跨度。
随机层级生成
在插入操作中有一个随机层级的生成操作,使用的随机函数zslRandomLevel
:
int zslRandomLevel(void) {
// 初始层数是1
int level = 1;
// 以 ZSKIPLIST_P 的概率提升层级,随机层数的值是0.25
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
// ZSKIPLIST_MAXLEVEL 最大层数是64
return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
删除
删除操作的核心在于找到要删除的节点,并更新相关的指针。
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--) { // 从最高层往下遍历
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; // 记录每层的前驱节点
}
x = x->level[0].forward; // 指向要删除的节点
if (x && score == x->score && sdscmp(x->ele, ele) == 0) { // 确认节点存在
zslDeleteNode(zsl, x, update); // 删除节点,并更新指针
if (!node)
zfree(x->ele); // 释放节点内存
zfree(x);
return 1; // 删除成功
}
return 0; // 节点不存在,删除失败
}
在删除操作中:
- 遍历各层,找到要删除节点的位置。
- 删除节点,并更新相关指针和跨度。
查找
查找操作相对简单,核心在于从高层到低层逐层遍历,直到找到目标节点或确认目标节点不存在。
unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {
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 &&
sdscmp(x->level[i].forward->ele, ele) < 0))) { // 查找目标节点
rank += x->level[i].span;
x = x->level[i].forward;
}
if (x->level[i].forward && score == x->level[i].forward->score &&
sdscmp(x->level[i].forward->ele, ele) == 0) { // 找到目标节点
rank += x->level[i].span;
return rank; // 返回目标节点的排名
}
}
return 0; // 目标节点不存在
}
在查找操作中:
- 从最高层开始,逐层向前移动,直到找到目标节点或确认其不存在。
- 返回目标节点的排名(或返回 0 表示不存在)。
在比较结点时,相应地有两个判断条件:
- 当查找到的结点保存的元素权重,比要查找的权重小时,跳表就会继续访问该层上的下一个结点。
- 当查找到的结点保存的元素权重,等于要查找的权重时,跳表会再检查该结点保存的 SDS 类型数据,是否比要查找的 SDS 数据小。如果结点数据小于要查找的数据时,跳表仍然会继续访问该层上的下一个结点。
总结
Redis 的跳表通过多级索引结构,实现了高效的插入、删除和查找操作。希望这篇文章能够帮助你更好地理解跳表的工作原理和实现细节。