数据结构
//跳跃表节点
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
robj *obj; // 成员是redis对象
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;
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
可以先看下有序集合的结构,其中就包含跳跃表的实现。后面再单独分析有序集合zset。
// 有序集合
typedef struct zset {
// 字典 key是成员 value是分值
dict *dict;
// 跳跃表 按分值排序的节点
zskiplist *zsl;
} zset;
先根据个人理解,画一张跳跃表的图,为了更好的理解下面的内容。
跳跃表除去“跳跃”的特性本身是个链表,“跳跃”由level这个层级化的信息中的指针带来的。
程序设计上,总是从高层级开始访问,相当于大范围的查找,随着查找范围缩小,慢慢的降低层级,有一种跳着查的感觉。
方法分析
zslCreateNode
创建跳跃表节点
// 层数level 分数score 对象是obj
zskiplistNode *zslCreateNode(int level, double score, robj *obj) {
// 同SDS一样,跳跃表节点分配空间时,要注意最后柔性数组的空间分配
zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
zn->score = score;
zn->obj = obj;
return zn;
}
zslCreate
创建一个跳跃表
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl)); //分配空间
zsl->level = 1; // 设置层级为1
zsl->length = 0; // 设置总长度为0
// 设置一个头结点
// 头结点层级最大 score为0 对象为空
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;
}
// 头结点的后退指针NULL
zsl->header->backward = NULL;
// 尾结点
zsl->tail = NULL;
return zsl;
}
zslRandomLevel
随机获取一个level,后面再细讲。
zslInsert
插入redis对象
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
redisAssert(!isnan(score));
x = zsl->header;
// 从当前的最高层级往下遍历
for (i = zsl->level-1; i >= 0; i--) {
// 理解rank先理解span,span是前一个节点到后一个节点的跨度
// 因为每个节点level高度不同,所以span跨度可能会是1,也可能一下子跨过多个节点
// 初始化rank[最高]=0
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
// 沿着前进指针遍历跳跃表
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
// 比较score,相同再比较obj
(x->level[i].forward->score == score &&
compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
// 累加前一个节点的span。最终得到从header到要插入的点的总距离。
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; // 这部分相连的肯定是header
update[i]->level[i].span = zsl->length; // 这部分header的跨越是整个跳跃表长度
}
// 更新表中节点最大层数
zsl->level = level;
}
// 创建新节点
x = zslCreateNode(level,score,obj);
// 将前面记录的指针指向新节点,并做相应的设置
for (i = 0; i < level; i++) {
// 新节点的forward指针指向相连节点的forward
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]);
// 更新新节点插入之后,沿途节点的 span 值
// 其中的 +1 计算的是新节点
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
// 未接触的节点的 span 值也需要增一,这些节点直接从表头指向新节点
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;
}
zslDeleteNode
删除指定节点
zslDelete
删除redis对象(先查找,再调用zslDeleteNode)
zslFreeNode
释放指定的跳跃表节点
zslFree
释放指定的跳跃表
// 其中遍历 所有节点的代码如下
// 因为对象都在第一层,只要一直遍历前进指针即可
while(node) {
next = node->level[0].forward;
zslFreeNode(node);
node = next;
}
跳跃表梳理
先看一个简单的有序链表:
在这样一个链表中,如果要查找某个数据,就需要从头开始逐个比较,直到找到指定的节点。时间复杂度是O(N)。
现在假设每两个节点,增加一个指针。在这样一个链表中,如果要查找某个数据,同样从头开始逐个比较,但是它一次会跳过两个节点,相比上一个链表,整体时间复杂度变成了O(N/2)。
按照这个思路,每三个节点再增加一个指针,让跳跃距离更大,这样就拥有了更快的查找能力。可以想象,当链表的长度比较大的时候,这样多个层次的链表进行“跳跃式”的查找,能跳过很多下层的节点,大大提高查找速度。
记下来思考,我们该怎么去构建这个跳跃链表,真的按照上面的想法,每几个节点一个循环这样有序的高度变化吗?
试想一下如果这么做,新插入一个节点,该怎么维持原先的循环有序呢,需要调整新节点以及后面所有的节点。删除一个节点同理。那么维护这样一个链表,就变的更加麻烦。实际上,Redis为了避免这个问题,采取的随机高度的做法,每个节点的高度都是随机性的,在整体上实现每个层次的数量级分化。
上面在读Redis的跳跃表时,有一个函数是zslRandomLevel
。具体代码如下:
// ZSKIPLIST_MAXLEVEL=32
// ZSKIPLIST_P=0.25
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
这个函数,在每个新增节点时都会调用,也就是每个节点都会随机出一个level高度。
怎么理解这件事。
函数中有一个循环,条件是随机数小于1/4,高度就增加1。从宏观角度上来看,假设有1000个节点,那么就大概会有250个节点高度大于1,62个节点高度大于2,16个节点高度大于3… 大致就是每一层之上的节点数大约是这一层节点数的
1/4,这样整体保证了这个跳跃表不会过于臃肿也不会失去跳跃性。
Redis取1/4,这个数字不是固定的,在自己的设计中,也可以更改,是一个弹性设计。
上图是一个Redis中可能生成的跳跃表示例,现在来查找9,需要的步骤是1->4->6->9,只需要4次就找到对应目标,查找示例如下