有序集合在咱们的平常生活中很是常见,好比根据成绩对学生进行排名、根据得分对游戏玩家进行排名等。对于有序集合的底层实现,咱们可使用数组、链表、平衡树等结构。数组不便于元素的插入和删除;链表的查询效率低,须要遍历全部元素;平衡树或者红黑树等结构虽然效率高但实现复杂。html
所以,Redis 采用了一种新的数据结构——跳跃表。跳跃表的效率堪比红黑树,可是其实现却远比红黑树简单。node
下面就开始咱们今天的学习之旅!git
按照惯例,咱们将下文中的涉及到的一些概念,在这里作一下简单介绍,方便后面的学习。github
0x
是一种标识,用来表示 16 进制。redis
F 是 16 进制中的 15,其二进制表示为 1111
。算法
FFFF 即 1111 1111 1111 1111
。数组
&0xFFFF
即与 0xFFFF
作位运算,只取低 16 位。markdown
跳跃表是 zset
(有序集合)的基础数据结构。跳跃表能够高效的保持元素有序,而且实现相对平衡树简单、直观。Redis 的跳跃表是基于 William Pugh 在 《Skip lists: a probabilistic alternative to balanced trees》 中描述的算法实现的。作了一下几点改动:数据结构
zrevrange
等命令。skiplist,首先它是一个 list。实际上,它是在有序链表的基础上发展起来的。dom
咱们先来看一下有序链表,有序链表是全部元素以递增或递减方式有序排列的数据结构,其中每一个节点又有指向下个节点的 next 指针,最后一个节点的 next 指针指向 NULL。递增有序链表示例以下:
如图所示,若是咱们想要查询值为 61 的元素,咱们须要从第一个元素开始依次向后查找、比较才能够找到,查找的顺序为 1 -> 11 -> 21 -> 31 -> 41 -> 51 -> 61,共 7 次比较,时间复杂度为 O(N)。有序链表的插入和删除操做都须要先找到合适的位置再修改 next 指针,修改操做基本不消耗时间,因此插入、删除、修改有序链表的耗时主要在查找元素上。
假如咱们 每相邻两个节点增长一个指针,让指针指向下下节点,以下图所示:
新增长的指针连成了一个新的链表,可是它包含的节点个数只有原来的一半(1,21,41,61)。如今当咱们想要查找 61 的时候,咱们就沿着这个新链表进行查找(绿色指针方向)。查找的顺序为 1 -> 21 -> 41 -> 61,共 4 次比较,须要比较的次数大概只有原来的一半。
利用一样的方式,咱们能够在上层新产生的链表上,继续为每相邻的两个节点增长一个指针,从而查看第三层链表,以下图所示:
新增长的指针连成了一个新的链表,它包含的节点个数只有第二层的一半(1,41)。如今当咱们想要查找 61 的时候,咱们沿着新链表进行查找(红色指针方向)。查找顺序为 1 -> 41,此时咱们发现 41 的 next 指针指向 null,咱们就开始从 41 节点的下一层开始查找(绿色指针方向),即 41 -> 61,连起来就是 1-> 41 -> 61,总共比较了 3 次,相比于上次查找又少了一次。当数据量大的时候,这种优点会更加明显。
skiplist 正是受这种 多层链表 的想法启发设计得来的。
按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就很是相似于一个 二分查找,使得查找的时间复杂度能够降到 O(logN)。
可是新插入一个节点以后,就会打乱上下相邻两层链表上节点个数严格的 2:1 的对应关系。若是要维持这种对应关系,就必须把新插入的节点后面的全部节点(也包括新插入的节点)从新进行调整,这会让时间复杂度从新退化为 O(N)。删除数据也有一样的问题。
skiplist 为了不这一问题,它 不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每一个节点随机出一个层数(level),新插入的节点就会根据本身的层数决定该节点是否在这层的链表上。
从上面咱们能够知道,跳跃表由多个节点构成,每一个节点由不少层构成,每层都有指向本层的下个节点的指针。
跳跃表主要涉及 server.h 和 t_zset.c 两个文件,其中在 server.h 中定义了跳跃表的数据结构,在 t_zset.c 中定义了跳跃表的节本操做。
接下来,让咱们一块儿来看一下跳跃表具体是如何实现的。
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
复制代码
该结构体包含以下属性:
level
数组中的 每项 包含如下两个元素:
跳跃表是 Redis 有序集合的底层实现方式之一。因此每一个节点的 ele 存储有序集合的成员 member 值,score 存储成员 score 值。全部节点的分值是按从小到大的方式排序的,当有序集合的成员分值相同时,节点会按 member 的字典序进行排序。
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
复制代码
该结构体中包含以下属性:
经过跳跃表结构体的属性咱们能够看到,程序能够在 O(1) 的时间复杂度下,快速获取到跳跃表的头结点、尾节点、长度和高度。
咱们已经知道了跳跃表节点和跳跃表结构体的定义,接下来咱们再看一下跳跃表的建立、插入、查找和删除操做。
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl)); //初始化内存空间
zsl->level = 1; //将层数设置为最小的 1
zsl->length = 0; //将长度设置为 0
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); //建立跳跃表头节点,层数为 ZSKIPLIST_MAXLEVEL=64 层
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) { //依次给头节点的每层赋值
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
zsl->header->backward = NULL; //头节点的回退指针设置为 NULL
zsl->tail = NULL; //尾节点设置为 NULL
return zsl;
}
复制代码
能够看到,跳跃表的建立过程以下:
首先声明一块大小为 sizeof(zskiplist)
的内存空间。
而后将层高 level
设置为 1,将跳跃表长度 length
设置为 0。而后建立头节点 header
,其中 ZSKIPLIST_MAXLEVEL
的定义以下:
#define ZSKIPLIST_MAXLEVEL 64
复制代码
表明层节点最高为 64 层,而咱们的头结点正是最高的层数。
头节点是一个特殊的节点,不存储有序集合的 member
信息。头节点是跳跃表中第一个插入的节点,其 level
数组的每项 forward
都 为NULL,span
值都为 0。
接着将头节点的回退指针 backward
和尾指针 tail
设置为 NULL。
这些都很好理解,就是初始化内存,而后依次将跳跃表结构体各个成员设置默认值。
建立跳跃表节点代码以下:
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
zskiplistNode *zn =
zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel)); //申请 zskiplistNode + 柔型数组(多层)大小的空间
zn->score = score; //设置节点分支
zn->ele = ele; //设置节点数据
return zn;
}
复制代码
建立跳跃表节点的代码也很好理解。
首先分配内存空间,这个空间大小为 zskiplistNode 的大小和 level 数组的大小。
zskiplistNode
结构体的最后一个元素为柔性数组,申请内存时须要指定柔性数组的大小,一个节点占用的内存大小为 zskiplistNode
的内存大小与 level
个 zskiplistLevel
的内存大小之和。
再将节点的 score
和 ele
分别赋值。
插入节点这块比较重要,也比较难懂,咱们仔细学习一下。
首先附上插入节点代码。
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; // update[] 数组用于存储被插入节点每层的前一个节点
unsigned int rank[ZSKIPLIST_MAXLEVEL]; // rank[] 数组记录当前层从 header 节点到 update[i] 节点所经历的步长。
int i, level;
serverAssert(!isnan(score));
x = zsl->header; //遍历的节点,因为查找被插入节点每层的前一个节点
for (i = zsl->level-1; i >= 0; i--) { //从上到下遍历
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; //给rank[] 数组初始值赋值,最上层从 header 节点开始,因此为 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))) //前进的规则存在 forward 节点且(forward 节点评分小于待插入节点评分 || (forward 节点评分等于待插入节点评分 && forward 节点元素字典值小于待插入节点元素字典值))
{
rank[i] += x->level[i].span; //加上 x 的跨度
x = x->level[i].forward; //节点向前前进
}
update[i] = x; // 将被插入节点当前层的前一个节点记录在 update[] 数组中
}
level = zslRandomLevel(); //随机生成一个层高
if (level > zsl->level) { //新生成节点的层高比当前跳跃表层高大事
for (i = zsl->level; i < level; i++) { //只更新高出的部分
rank[i] = 0; //由于是头结点,因此为 0
update[i] = zsl->header; //该层只有头结点
update[i]->level[i].span = zsl->length; //由于 forward 指向 NULL,因此跨度应该是跳跃表全部的节点,因此 span 为跳跃表的长度
}
zsl->level = level; //更新跳跃表的层高
}
x = zslCreateNode(level,score,ele); // x 被赋值成新建立的节点
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward; //更新 x 节点的 level[i] 层的 forward 指针
update[i]->level[i].forward = x; //更新 update[i] 节点的 level[i] 层的 forward 指针
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); //更新 x 节点的 level[i] 层的跨度 span
update[i]->level[i].span = (rank[0] - rank[i]) + 1; //更新 update[i] 节点的 level[i] 层的跨度 span
}
for (i = level; i < zsl->level; i++) { //当新插入节点的层高比跳跃表的层高小时,须要更新少的几层的 update[] 节点的跨度,即 +1
update[i]->level[i].span++;
}
x->backward = (update[0] == zsl->header) ? NULL : update[0]; //更新 x 的 backward 指针,若是 update[0] 是头结点则为 NULL,不然为 update[0]
if (x->level[0].forward) // 更新 x 节点第 0 层有后续节点,则后面节点的 backward 指向 x 节点,不然的话 x 节点为最后一个节点,须要将 tail 指针指向 x 节点
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++; //跳跃表的长度 +1
return x;
}
复制代码
咱们来看一下跳跃表实现过程示意图:
假设咱们想插入一个节点 45。咱们首先须要找到插入的位置,而后再更改由于节点插入致使受影响的位置,好比跳跃表的 level,前一个节点的每层的 forward 指针等等。
在下图中,我用红色标出哪些位置受了影响须要修改。
所以咱们把插入节点的步骤总为以下几点:
如今咱们来思考以下几个问题:
为何须要先查找要插入的位置,而后再调整跳跃表的高度?
由于咱们是根据跳跃表高度来查找节点的,首先咱们要找到最高的一层,而后一层一层向下查找,直到找到节点。当新插入的节点的 level 比跳跃表的 level 大的时候,若是先调整跳跃表高度,而后咱们就会以调整后的高度为起点,而后向后查找,可是该层的 forward 指针指向 NULL,咱们是找不到节点的。
如何调整受影响节点和新插入节点每层的 forward 指针和 span?
按照上述思路,接下来让咱们逐步研究插入节点代码。
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
复制代码
定义两个数组:
x = zsl->header;
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;
}
复制代码
按照上述代码逻辑,值为 2五、3五、45 的节点查找插入位置的查找路线以下图所示:
接下来咱们一步一步分析代码。
for (i = zsl->level-1; i >= 0; i--)
复制代码
for 循环的起始值为 zsl->level-1
,正验证了上面咱们所说的,节点查询要从最高层开始查找,查找不到再从下一层开始查询。
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
复制代码
rank[]
数组的做用是记录当前层从 header 节点到 update[i] 节点所经历的步长。
从上图咱们能够看到,节点查找路线是 “右->下->右->下” 这种的。
在最高层的时候,咱们的起始位置确定是 header
节点,此时该节点与 header 节点之间的距离为 0,因此 rank[zsl->level-1]
的值确定为 0。
当咱们向下层走的时候,其实是从上面一层查到的最后一个节点下来的,好比上图中查找值为 45 的节点的时候,当咱们从第四层下到第三层的时候,是从 41 节点开始查的,rank[2] 的值同第四层的值 rank[3]。
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;
复制代码
这段代码说明了咱们寻找节点插入位置的两条比较原则:
即咱们提到的,评分不相等时比较评分,评分相等值比较值的字典排序,不会出现两个都相等的状况。
接着记录步长 rank[i]
,rank[i]
的值即为当前节点的步长 rank[i]
加上该节点到下一节点的跨度 x->level[i].span
。
节点向前移动到下一个节点。
当一层走完循环以后,此时应该知足两种状况:
x->forward == NULL
x->forward != NULL && (x->forward.score > score || (x->forward.score == score && sdscmp(x->level[i].forward->ele,ele) > 0))
此时咱们应该向从下一层开始寻找了,那么咱们应该记住受影响的节点,也是插入节点每层的前一个节点 update[i] = x
。
循环直到第一层结束,此时咱们已经找到了要插入的位置,并将插入节点每层的前一个节点记录在 update[]
数组中,并将 update[]
数组中每一个节点到 header 节点所经历的步长也记录了下来。
咱们以 length=3 level=2 的一个跳跃表插入节点为例,update 和 rank 赋值后跳跃表以下:
level = zslRandomLevel();
复制代码
每一个节点的层高是随机生成的,即所谓的 几率平衡,而不是 强制平衡,所以,对于插入和删除节点比传统上的平衡树算法更为简洁高效。
生成方法以下:
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) //ZSKIPLIST_P=0.25
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
复制代码
上述代码中,level 的初始值为 1,经过 while 循环,每次生成一个随机值,取这个值的低 16 位做为 x,当 x 小于 0.25 倍的 0XFFFFFF 时,level 的值加 1;不然退出 while 循环,最终返回 level 和 ZSKIPLIST_MAXLEVEL 二者中的最小值。
下面计算节点的指望层高。假设 p = ZSKIPLIST_P;
因此节点的指望层高为:
当 p=0.25 时,跳跃表节点的指望层高为 1/(1-0.25)≈1.33。
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;
}
复制代码
只有当待插入节点的层高比当前跳跃表的层高大时,才会进行该操做。
zsl->level = level;
跳跃表的层高赋值为最高的层高,这是没有问题的。
咱们接着以该图为例:
第 0 层和第 1 层咱们已经更新过了,所以咱们只须要从未更新过的层开始便可,即 i = zsl->level;
,从第 2 层开始。第 2 层只须要更新 header 节点,因此 update[i] = zsl->header
。而 rank[i]
则为 0。
update[2]->level[2].span
的值先赋值为跳跃表的总长度,后续在计算新插入节点 level[2]
的 span
时会用到此值。在更新完新插入节点 level[2]
的 span
以后会对 update[2]->level[2].span
的值进行从新计算赋值。
至于为何将 update[2]->level[2].span
的值设置为跳跃表的总长度,咱们能够从 span
的定义来思考。span
的含义是 forward 指向的节点与本节点之间的元素个数。而 update[2]->level[2].forward
指向的是 NULL 节点,中间隔着的是跳跃表的全部节点,所以赋值为跳跃表的总长度。
调整高度后的跳跃表以下图所示:
x = zslCreateNode(level,score,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++;
}
复制代码
forward 值的修改很好理解,就是简单的链表插入节点。
那么如何理解 update[i]->level[i].span - (rank[0] - rank[i])
和 (rank[0] - rank[i]) + 1
呢?
咱们对照下图来深刻理解一下 span 赋值过程。
首先,咱们应该对一下几点有所了解:
咱们以 update[1] 节点举例,其余节点原理也是如此。
因此,rank[0] - rank[1]
实际上就是节点 update[1]
到 update[0]
的距离(score=1 的节点到 score=21 的节点的距离)
update[1]->level[1].span
的值表示在第一层 update[1] 节点与指向的节点之间的跨度,从上图咱们能够看到,这段距离中包含 update[1]
到 update[0]
的距离,剩下的距离就是 新插入节点到 update[1]->level[1].forward
节点之间的距离。
由于新插入的节点是在 update[0] 后面插入的,所以 update[0]
和 新插入节点 之间的距离为 1,rank[0] - rank[1] + 1
即为 update[1]->level[1].span
的值。
咱们把问题抽象化一下:
假设有节点 A 和 B,在他们中间插入 X,
rank[0] - rank[i]
计算的就是 A 到 X 的前一个节点 X-1 的距离;update[i]->level[i].span
计算的就是 A 到 B 的距离;update[i]->level[i].span - (rank[0] - rank[i])
计算的就是 X 到 B 的距离。update[i]->level[i].span = (rank[0] - rank[i]) + 1
计算的是 A 到 X-1 再 +1,表示的是 A 到 X 的距离。计算的原则是 左开右闭。
按照上述算法,咱们来实际走一遍插入过程。level 的值为 3,因此能够执行三次 for 循环,插入过程以下:
第一次 for 循环
插入节点并更新第 0 层后的跳跃表以下图所示:
第二次 for 循环
插入节点并更新第 1 层后的跳跃表以下图所示:
第三次 for 循环
插入节点并更新第 2 层后的跳跃表以下图所示:
新插入节点的高度大于原跳跃表高度,因此下面代码不会运行。但若是新插入节点的高度小于原跳跃表高度,则从 level 到 zsl->level-1 层的 update[i] 节点 forward 不会指向新插入的节点,因此不用更新 update[i] 的 forward 指针,只将这些 level 层的 span 加 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;
复制代码
根据 update 的赋值过程,新插入节点的前一个节点必定是 update[0],因为每一个节点的后退指针只有一个,与此节点的层数无关,因此当插入节点不是最后一个节点时,须要更新被插入节点的 backward 指向 update[0]。若是新插入节点是最后一个节点,则须要更新跳跃表的尾结点为新插入节点。插入及诶单后,更新跳跃表的长度加 1.
插入新节点后的跳跃表以下图所示:
有了上面插入节点的学习,对于节点的删除,咱们应该更容易理解了。
咱们把删除节点简单的分为两步:
删除节点代码以下:
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].level[i] 的 forward 节点是 x 的状况,须要更新 span 和 forward
update[i]->level[i].span += x->level[i].span - 1;
update[i]->level[i].forward = x->level[i].forward;
} else {// update[i].level[i] 的 forward 节点不是 x 的状况,只须要更新 span
update[i]->level[i].span -= 1;
}
}
if (x->level[0].forward) { // 若是 x 不是尾节点,更新 backward 节点
x->level[0].forward->backward = x->backward;
} else { // 不然 更新尾节点
zsl->tail = x->backward;
}
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--; //更新跳跃表 level
zsl->length--; // 更新跳跃表长度
}
复制代码
查找须要删除的节点要借助 update 数组,数组的赋值方式与 插入节点 中的 update 的赋值方式相同,再也不赘述。查找完毕以后,update[2]=header,update[1] 为 score=1 的节点,update[0] 为 score=21 的节点。删除节点前的跳跃表以下图所示:
设置 span 和 forward 的代码以下:
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;
}
}
复制代码
咱们先来看 span 的赋值过程。删除节点时 span 的赋值以下图所示:
假设咱们想要删除 score=21 的节点,那么 update[0] 和 update[1] 应该为 score=1 的节点,update[2] 应该为头节点。
现更新节点的 span 和 forward 分为两种状况:
update[i] 的第 i 层的 forward 节点指向 x(如上图 update[0]->level[0])
update[0].level[0].span
是 update[0] 到 x 的距离;x.level[0].span
是 x 到 x.level[0].forward 之间的距离;update[0].level[0].span + x.level[0].span
是 update[0] 到 x.level[0].forward 之间的距离;update[0].level[0].span + x.level[0].span - 1
是删除 x 节点后 update[0] 到 x.level[0].forward 之间的距离;update[0].level[0].forward
即为 x.level[0].forward。update[i] 的第 i 层的 forward 节点指向 x(如上图 update[1]->level[1])
设置 span 和 forward 后的跳跃表以下图所示:
update 节点更新完毕以后,须要更新 backward 指针、跳跃表高度和长度、若是 x 不为最后一个节点,之间将第 0 层后一个节点的 backward 赋值为 x 的backward 便可;不然,将跳跃表的尾指针指向 x 的 backward 节点便可。代码以下:
if (x->level[0].forward) {
x->level[0].forward->backward = x->backward;
} else {
zsl->tail = x->backward;
}
复制代码
当删除的 x 节点是跳跃表的最高节点,而且没有其余节点与 x 节点的高度相同时,须要将跳跃表的高度减 1。
因为删除了一个节点,跳跃表的长度须要减 1。
删除节点后的跳跃表以下图所示:
删除跳跃表就比较简单了。获取到跳跃表对象以后,从头节点的第 0 层开始,经过 forward 指针逐步向后遍历,没遇到一个节点便将其释放内存。当全部节点的内存都被释放以后,释放跳跃表对象,即完成了跳跃表的删除操做。代码以下
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);
}
复制代码
在 Redis 中,跳跃表主要应用于有序集合的底层实现(有序集合的另外一种实现方式为压缩列表)。
在 redis.conf 有关于有序集合底层实现的两个配置:
zset-max-ziplist-entries 128 // zset 采用压缩列表时,元素个数最大值。默认值为 128。
zset-max-ziplist-value 64 // zset 采用压缩列表时,每一个元素的字符串长度最大值,默认为 64。
复制代码
zset 添加元素的主要逻辑位于 t_zset.c 的zaddGenericCommand
函数中。zset 插入第一个元素时,会判断下面两种条件:
zset-max-ziplist-entries
的值是否等于 0;zset-max-ziplist-value
小于要插入元素的字符串长度。知足任一条件 Redis 就会采用跳跃表做为底层实现,不然采用压缩列表做为底层实现方式。
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 新插入元素时,会判断如下两种条件:
zset_max_ziplist_entries
;zset_max_ziplist_value
。当慢如任一条件时,Redis 便会将 zset 的底层实现由压缩列表转为跳跃表,代码以下:
if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries ||
sdslen(ele) > server.zset_max_ziplist_value)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
复制代码
值得注意的是,zset 在转为跳跃表以后,即便元素被逐渐删除,也不会从新转为压缩列表。
本章咱们介绍了跳跃表的演变过程、基本原理、和实现过程。
演变过程就是在链表的基础上,间隔抽取一些点,在上层造成一个新的链表,相似于二分法,达到时间减半的效果,可是又不一样于二分法,由于新插入的节点的层高是随机生成的,即所谓的 几率平衡,这样保证了跳跃表的查询、插入、删除的平均复杂度都为 O(logN)。
跳跃表的实现过程,咱们着重讲了插入节点,其中咱们引入了两个数组,update[] 和 rank[] 数组,咱们须要对这两个数组特别理解,才能理解插入过程。
看到这了,咱们不妨问本身几个问题:
若是你们可以对这些问题解答出来,相信你们已经对跳跃表了如指掌了。