「这是我参与2022首次更文挑战的第29天,活动详情查看:2022首次更文挑战」。
Sorted-Set底层存储数据结构
3.1.1 zskiplistNode结构
zskiplistNode是跳表的结点结构,数据结构如下:
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
结构属性介绍:
- ele:存的是集合的值
- score:存的一个double类型的排序字段,通过这个字段来进行集合值的排序
- backward:链表指针,指向当前元素的前一个元素
- Level[]:跳表的高度
- forward:指向下一个元素
- span:跨度,当前结点到下一个结点中间元素的个数
3.1.2 zskiplist结构
zskiplist就是跳表,它用来管理整个跳表的高度、长度、头结点、尾结点,其数据结构如下:
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
结构属性介绍:
- header:指向跳表的头结点
- tail:指向跳表的尾结点
- length:跳表的高度
- level:当前调整的高度
这两个结构就组成了我们Redis中的跳表的数据结构,下面我们先通过一张Redis的Sorted-Set图(图3.1)来分别解释一下每个属性具体的含义:
3.2 Sorted-Set高度算法探究
根据第二节跳表的形成我们了解到可以提取结点来做多层索引来提高查询的效率,我们怎么来确定跳表的高度呢?对于结点不插入和删除,我们又怎么来实时调整跳表的高度呢?我们来看一下Redis是怎么做的。
//跳表最高层级为64
#define ZSKIPLIST_MAXLEVEL 64 /* Should be enough for 2^64 elements */
//跳表的随机因子为 0.25
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
/* Returns a random level for the new skiplist node we are going to create.
* The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
* (both inclusive), with a powerlaw-alike distribution where higher
* levels are less likely to be returned. */
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
根据代码我们先回答一下第一个问题:怎么来确定跳表的高度?回答:随机生成,对没有看错,就是随机生成,但是为了保证查询的性能,我们尽量让找出一套算法能够达到一个O(log(N))的时间复杂度,我们先看一下这个函数,random()*会生成一个*32位的随机数,跟 0xFFFF做与操作,其实就是把高16位清零,得到一个处于0x0000-0xFFFF的数字,然后每次循环判断生产的这个数字是否小于自己的1/4,如果成立则高度+1,循环结束在判断一下生产的level是否小于 64层,小于的话,就用生成的层数,否则用64层。
我们看一下推导过程,我们定义随机因子为p:
- 结点层数至少为1,而大于1的结点层数,满足一个概率分布。
- 结点层数恰好等于1的概率为1 - p
- 结点层数大于等于2的改为为p,而结点层数等于2的概率为p * (1 - p)
- 结点层数大于等于3的概率为p * p,而结点层数等于3的概率为p * p * (1-p)
- 结点层数大于等于4的概率为p * p * p,而结点层数等于4的概率为p * p * p * (1-p)
- 结点层数大于等于5的概率为p * p * p * p,而结点层数等于5的概率为p * p * p * p * (1-p)
- …
那么,一个结点的平均层数的计算公式如下:
Redis定义的随机因子p是 1/4,那通过这个公式能够得平均高度是:1.33。
3.3 Sorted-Set头结点初始化源码分析
源代码:创建一个skiplist
/* Create a new skiplist. */
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
//先申请一块内存
zsl = zmalloc(sizeof(*zsl));
//默认给跳表的高度赋值为1
zsl->level = 1;
//默认给跳表的长度赋值为0
zsl->length = 0;
//创建一个头结点,然后跳表指向头结点,可参考函数zslCreateNode
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;
//跳表尾部指向为NULL
zsl->tail = NULL;
return zsl;
}
创建一个skiplistNode结点
//创建一个skiplistNode结点,有三个参数:level: 层级,score: 排序key,sds: 跳表值
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
//同样先通过传过来的高度申请结点的内存
zskiplistNode *zn =
zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
//给跳表结点score赋值
zn->score = score;
//给跳表结点ele赋值
zn->ele = ele;
return zn;
}
我们通过图3.2来了解一下,头结点初始化后结构是什么样的。
3.4 Sorted-Set插入元素源码分析
3.4.1 插入函数解析
下面我们来通过分析一个元素的插入流程来详细的了解一下源码,我们先看一下整个函数,然后我们会逐步的拆解每一次循环的含义。
/* 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'. */
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score));
//赋值跳表的头结点给变量x
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;
}
/* 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();
//如果新节点的层数比表中其他节点的层数都要大
//那么初始化表头节点中未使用的层,并将它们记录到 update 数组中
//将来也指向新节点
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++) {
//设置新节点的 forward 指针
x->level[i].forward = update[i]->level[i].forward;
//将旧结点的各个节点的 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;
}
/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
//调整新结点的backward
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;
}
通过对整体函数的解析,我们发现插入一个元素基本上分为4个步骤:
1、查到插入的位置(查询过程);
2、生产新结点高度并调整跳表的高度;
3、插入元素;
4、调整backward并更新跳表的长度;