1 概述
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,或者有序集合中的元素是比较长的字符串,Redis就会使用跳跃表来作为有序集合键的底层实现。
2 跳跃表数据结构解析
如上图所示,跳跃表有很多层组成。头节点中有一个64层结构,每层结构都包含指向本层的下一个节点的指针,指向本层下一个节点中间所跨越的节点个数为层的跨度(span)。除了头节点外,层数最多的节点的层高为跳跃表的高度(level),每一层都是一个有序链表,即数据是递增的。另外除头节点外,如果一个元素在上层有序链表中出现,则它一定会在下层有序链表中出现。跳跃表每层最后一个节点指向null,表示本层有序链表的结束。跳跃表拥有一个尾指针,指向跳跃表最后一个节点。最底层的有序链表包含所有节点,最底层的节点个数为跳跃表的长度。跳跃表每个节点含有多个指向其他节点的指针,所以在跳跃表进行查找、插入和删除操作时可以跳过一些节点,快速找到需要操作的节点,这是以牺牲空间的形式来达到快速查找的目的。
跳跃表节点结构体源码如下:
typedef struct zskiplistNode {
//用于存储字符串类型的数据
sds ele;
//用于存储排序的分值
double score;
//后向指针,只能指向当前节点最底层的前一个节点,头结点和第一个节点
struct zskiplistNode *backward;
struct zskiplistLevel {
//指向本层下一个节点,尾节点的forward指向null
struct zskiplistNode *forward;
//forward指向的节点与本节点之间的元素个数。span的值越大,跳过的节点个数就越多
unsigned long span;
} level[];
} zskiplistNode;
跳跃表是Redis有序集合的底层一种实现方式,所以每个节点的ele存储有序集合元素的值,score存储元素的score值。所有节点的score值是按照从小到大排序的。
除了跳跃表节点外,还需要一个跳跃表结构来管理节点。Redis使用zskiplist结构体,源码如下:
typedef struct zskiplist {
/*header:指向跳跃表头节点。头节点是跳跃表的一个特殊节点,它的level数组元素个数为64.
* 头节点在有序集合中不存储任何元素和score值,ele值为null,score值为0;不计入
* 跳跃表的总长度。头节点在初始化时,64个元素的forward都指向null,span值都为0
* */
//tail 指向跳跃表尾节点
struct zskiplistNode *header, *tail;
//跳跃表长度
unsigned long length;
//跳跃表高度
int level;
} zskiplist;
3 跳跃表初始化
初始化源码如下:
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;
}
/* Create a new skiplist. */
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
//分配空间
zsl = zmalloc(sizeof(*zsl));
//默认层数等于1
zsl->level = 1;
//0个节点
zsl->length = 0;
//redis中最大32层级
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;
}
如上zslCreate函数的源码用于初始化一个跳跃表,Redis中实现的跳跃表最高允许32层索引,这么做也是为了性能与内存之间的均衡,过多的索引层必然占用更多的内存空间,32是一个比较合适的值。
4 插入节点
插入节点源码如下:
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
//upate数组用于记录新节点在每一层索引的目标插入位置
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
//记录目标节点每一层的排名
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score));
//指向哨兵节点
x = zsl->header;
//循环找到最后一个小于当前给定score值的节点
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
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;
}
/* 获取一个均衡跳跃表的level值,标志着新节点将要在哪些索引中出现 */
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;
}
//根据score和ele创建节点
x = zslCreateNode(level,score,ele);
//每一索引层都要插入新节点
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward;
//rank[0]表示新节点在最底层链表的排名,就是它前面有多少个节点
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;
}
/* 自增span值,因为新插入了一个节点,跨度自然要增加 */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
//修改backward 指针与tail指针
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。
5 删除节点
删除节点的源码如下:
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].span += x->level[i].span - 1;
update[i]->level[i].forward = x->level[i].forward;
} else {
update[i]->level[i].span -= 1;
}
}
if (x->level[0].forward) {
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--;
zsl->length--;
}
6 zslGetRank查询
获取排名函数的源码如下:
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;
}
/* x 有可能等于zsl->header 所以判断是否为非null*/
if (x->ele && sdscmp(x->ele,ele) == 0) {
return rank;
}
}
return 0;
}
7 ZipList和跳跃表的选择
在Redis中,跳跃表主要应用于有序集合的底层实现,而另一种事ZipList。
Redis的Zset之所有这样设计,是因为当ZipList变得很大的时候,存在几个缺点:
1、每次插入和修改引发的realloc操作会有更大的概率造成内存复制,从而减低性能。
2、一旦发生内存复制,成本就会相应增加,因为要复制更大的一块数据。
3、当ZipList数据项过多时,在它里面查找指定的数据项的性能就会变得很低,因为在ZipList中查找需要进行逐个节点的遍历。
redis.conf相关配置如下:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
zset-max-ziplist-entries:表示元素个数的最大值,默认值为128.
zset-max-ziplist-value :表示每个元素的字符串长度最大值,默认为64.
如上配置表示当元素个数大于128或者每一个元素的长度大于64字节的时候,底层数据结构就会从ziplist切换到skiplist。
底层数据结构切换的源码如下:
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
//创建跳跃表结构
zobj = createZsetObject();
} else {
//创建ziplist压缩列表结构
zobj = createZsetZiplistObject();
}