这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
1. 简介
1.1 基本思想
通过将有序集合的部分节点分层,由最上层开始依次向后查找,如果本层的 next 节点大于待查找目标值或 next 节点为 NULL ,则从本节点降低一层向后继续查找,依次类推,找到目标值则返回节点,否则返回 NULL
下图中,总共查找了4次就可以找到55,比有序链表少了两次。当数据量大时,效果会更加明显
1.2 实现过程
跳跃表的性质:
- 跳跃表由多层构成
- 跳跃表有一个头节点 header ,头节点中有64层
- 节点每层的结构包含指向下个节点的指针,指向本层下个节点所跨越的节点个数为本层的跨度 span
- 除头结点外,层数最多的节点的层高为跳跃表的高度 level ,如图示高度为3
- 每层都是一个有序链表,数据递增
- 除头节点外,如果一个节点在上层的有序链表出现过,则下层一定也会出现该节点
- 每层最后一个节点指向 NULL ,表示本层有序链表的结束
- 跳跃表拥有一个尾结点 tail ,指向跳跃表最后一个节点
- 最底层的有序链表包含所有节点,该层节点个数为跳跃表的长度 length ,如图示长度为7
- 每个节点包含一个后退指针,指向最底层的前一个节点,头节点和第一个节点的后退指针为 NULL
跳跃表中每个节点维护了多个指向其他节点指针,所以在进行查找、插入、删除操作时可以跳过一些节点,快速找到需要操作的节点。跳跃表以牺牲空间换取时间的方式达到快速查找的目的
2. 数据存储结构
跳跃表为 Redis 有序集合的底层实现之一,每个节点的 ele 存储有序集合的成员 member 值,score 存储成员的 score 值。所有节点的分值从小到大排序,当分值相同时,节点会根据 member 的字典序排序
跳跃表能在 O(1) 的时间复杂度下,快速获取到头节点、尾结点、长度、高度
2.1 跳跃表
跳跃表的 zskiplist 结构体
// server.h
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
- header:指向跳跃表头节点的指针
- 头节点为特殊的节点,它的 level 大小为64
- 头节点不存储任何 member 和 score 值,ele 值为 NULL ,score 为0
- 不计入跳跃表的长度
- 头节点初始化时,64个元素的 forward 都指向 NULL ,span 为0
- tail:指向跳跃表尾结点的指针
- length:跳跃表的长度,表示除头节点外的节点总数
- level:跳跃表的高度
2.2 跳跃表节点
跳跃表节点的 zskiplistNode 结构体
// server.h
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
- ele:存储字符串类型的数据
- score:存储排序的分值
- backward:后退指针,指向当前节点在最底层的前一个节点,头节点和第一个节点的 backward 指向 NULL 。从后向前遍历跳跃表时使用
- level:柔性数组。每个节点的 level 的大小不一样,在创建节点时,随机生成一个1~64的值决定 level 的大小,值越大出现概率越小
- forward:指向本层下一个节点的指针,尾结点的 forward 指向 NULL
- span:当前节点指向本层下个节点所跨越的节点个数。span 越大,跨越的节点越多
3. 源码浅析
3.1 创建跳跃表
3.1.1 节点层高
创建节点时,会通过 zslRandomLevel 函数随机生成一个1~64的值作为节点的高度,值越大概率越小。节点的高度确定后就不会更改
// server.h
#define ZSKIPLIST_MAXLEVEL 64
#define ZSKIPLIST_P 0.25
// t_zset.c
int zslRandomLevel(void) {
// 初始高度为1
int level = 1;
// 随机获取一个值,取该值的低16位作为 x
// 当 x 小于0.25倍 0xFFFF 时,高度加1,否则退出循环
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
// 取 level 和 ZSKIPLIST_MAXLEVEL 中的最小值作为节点高度
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
3.1.2 创建节点
创建节点时,需要确定高度、分值、member
// t_zset.c
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
// 申请内存
zskiplistNode *zn =
zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
// 设置节点的分值
zn->score = score;
// 设置节点的保存的字符串内容
zn->ele = ele;
return zn;
}
3.1.3 创建跳跃表
// t_zset.c
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
// 申请内存
zsl = zmalloc(sizeof(*zsl));
// 初始层高 level=1
zsl->level = 1;
// 初始长度 length=0
zsl->length = 0;
// 创建头节点,头节点的分值 score=0 、字符串内容 ele=NULL 、层高 level=64
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
// 遍历头节点的层级,设置每层指向的下一个节点 forward=NULL ,跨度 span=0
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
// 设置头节点的后退节点 backward=NULL
zsl->header->backward = NULL;
// 设置尾节点 tail=NULL
zsl->tail = NULL;
return zsl;
}
3.2 插入节点
插入节点的步骤:
- 查找插入的位置
- 调整跳跃表的高度(有需要时)
- 插入节点
- 调整节点的 backward(有需要的节点)
假设已存在一个跳跃表,需要插入一个 score=40 的节点,下面为初始跳跃表:
3.2.1 查找插入的位置
// t_zset.c
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
// update[]:记录待插入节点在每层的前一个节点
// rank[]:记录每层的头节点到 update[i] 节点的跨度
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score));
x = zsl->header;
// 从头节点开始,遍历跳跃表的层级(高到低),统计出 update[] 和 rank[]
for (i = zsl->level-1; i >= 0; i--) {
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;
}
// ......
}
根据上述代码统计出 update 和 rank:
3.2.2 调整跳跃表高度
节点的层高是随机的,假设待插入节点的层高为3,大于跳跃表当前高度2,则需要调整跳跃表的高度
// t_zset.c
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
// ......
// 随机获取待插入节点高度
level = zslRandomLevel();
// 如果待插入节点高度大于跳跃表当前高度,则需要调整跳跃表高度
if (level > zsl->level) {
// 遍历跳跃表当前高度到待插入节点高度
// 统计新增层级的 update 和 rank
// 设置新增层级的头节点的跨度 span 为跳跃表当前长度,后续使用
for (i = zsl->level; i < level; i++) {
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = zsl->length;
}
// 更新跳跃表高度
zsl->level = level;
}
// ......
}
根据上述代码调整跳跃表高度、统计和设置相关值:
3.2.3 插入节点
// t_zset.c
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
// ......
// 创建新节点
x = zslCreateNode(level,score,ele);
// 从低到高遍历层级,插入新节点(类似链表插入节点)
for (i = 0; i < level; i++) {
// 设置新节点在该层级的 forward
x->level[i].forward = update[i]->level[i].forward;
// 设置新节点在该层级的前一个节点的 forward
update[i]->level[i].forward = x;
// 设置新节点在该层级的 span
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
// 设置新节点在该层级的前一个节点的 span
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
// 当新节点的层高小于跳跃表高度时
// 需要调整新节点层高到跳跃表高度之间层级的 update[i] 的跨度 span
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
// ......
}
根据上述代码插入节点:
3.2.4 调整 backward
// t_zset.c
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
// ......
// 设置新节点的 backward
// 如果新节点在最底层的前一个节点为头节点,则 backward=NULL
// 否则 backward=update[0]
x->backward = (update[0] == zsl->header) ? NULL : update[0];
// 如果新节点在最底层的下一个节点存在,则下一个节点的 backward 为该新节点
// 否则设置跳跃表的尾结点 tail 为该新节点
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
// 跳跃表长度加1
zsl->length++;
return x;
}
调整 backward:
3.3 删除节点
删除节点的步骤:
- 查找需要更新的节点
- 调整 span、forward、backward
假设存在3.2.4节的跳跃表,删除 score=40 的节点 x
3.3.1 查找需要更新的节点
// t_zset.c
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 = x->level[0].forward;
// 如果 x 节点存在,且是待删除节点,则进一步执行删除逻辑
if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
// 删除节点
zslDeleteNode(zsl, x, update);
// node 存在时,需要将删除节点 x 赋值给 node
// node 不存在时,释放删除节点 x
if (!node)
zslFreeNode(x);
else
*node = x;
return 1;
}
// 未找到待删除节点,返回0
return 0;
}
删除节点前:
3.3.2 调整 span、forward、backward
// t_zset.c
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
int i;
// 遍历跳跃表层级(低到高)
for (i = 0; i < zsl->level; i++) {
if (update[i]->level[i].forward == x) {
// 如果 update[i] 的下一个节点是待删除节点 x ,则更改其下一个节点 forward 和跨度 span
update[i]->level[i].span += x->level[i].span - 1;
update[i]->level[i].forward = x->level[i].forward;
} else {
// 如果 update[i] 的下一个节点不是待删除节点 x ,则更改其跨度 span(待删除节点的高度小于跳跃表高度的场景)
update[i]->level[i].span -= 1;
}
}
if (x->level[0].forward) {
// 如果待删除节点在最底层的下一个节点存在,则更新其 backward
x->level[0].forward->backward = x->backward;
} else {
// 如果待删除节点在最底层的下一个节点为 NULL ,则更新跳跃表尾结点
zsl->tail = x->backward;
}
// 遍历跳跃表层级(高到低),调整跳跃表高度
// 如果当前层级头节点的下一个节点为 NULL ,则高度减1,直到不满足该条件是停止调整
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--;
// 跳跃表长度减1
zsl->length--;
}
调整 span、forward、backward 后:
3.4 删除跳跃表
// t_zset.c
void zslFree(zskiplist *zsl) {
zskiplistNode *node = zsl->header->level[0].forward, *next;
// 释放头节点
zfree(zsl->header);
// 从头到尾遍历最底层每个节点并删除节点
while(node) {
next = node->level[0].forward;
zslFreeNode(node);
node = next;
}
// 释放跳跃表
zfree(zsl);
}
4. 跳跃表的应用
Redis 中,跳跃表主要应用于有序集合的实现(另一种实现为压缩列表)
关于有序集合的配置:
- zset_max_ziplist_entries:128
- zset_max_ziplist_value:64
4.1 初始有序集合
// t_zset.c
void zaddGenericCommand(client *c, int flags) {
// ......
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
// 创建跳跃表结构
zobj = createZsetObject();
} else {
// 创建压缩链表结构
zobj = createZsetZiplistObject();
}
// ......
}
- 当 zset_max_ziplist_entries 设置为 0
- 插入元素的字符串长度大于 zset_max_ziplist_value
上面两个条件满足其一时,初始的 zset 为跳跃表结构。一般情况下都不满足,所以默认结构为压缩链表
4.1 有序集合变更结构
// t_zset.c
int zsetAdd(robj *zobj, double score, sds ele, int *flags, double *newscore) {
// ......
if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
if (sdslen(ele) > server.zset_max_ziplist_value)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
}
- 跳跃表长度大于 zset_max_ziplist_entries
- 插入元素的长度大于 zset_max_ziplist_value
上面两个条件满足其一时,zset 的压缩链表结构变更为跳跃表。变更结构后,删除节点时也不会重新转变为压缩链表
跳跃表的查询、插入、删除操作的平均时间复杂度为 O(logN)
学自《Redis 5设计与源码分析》