1、Redis 的 zset 是一个复合结构,结构特点:
1)需要通过hash 结构来存储 value 和 score 的对应关系
2)要支持按照 score 来排序
3)还要按照指定 score 的范围来获取 value 列表的
4)要实现上诉的功能的结构,作者想到了跳跃列表
5)zset的结构:通过一个hash字典加跳跃列表skiplist
2、跳跃列表结构
1)基本结构
解释:Redis的跳跃列表总共有64层,上图中每个kv对应的结构如下图中的zslnode结构,kv header也是zslnode结构,只不过value字段是null,score值是Double.MIN_VALUE,相邻来个kv之间用指针窜接起来,形成双向列表,而且是有序的,从小到大,每一层元素的遍历是从kv header开始
2)结构体
struct zsl { zslnode* header; // 跳跃列表头指针 int maxLevel; // 跳跃列表当前的最高层 map<string, zslnode*> ht; // hash 结构的所有键值对 } struct zslnode { string value; double score; zslnode*[] forwards; // 多层连接指针 zslnode* backward; // 回溯指针 } |
2、查找
1)如果要定位到紫色的模块,首先从header往下定位,比header降一层,在定位比上层降一层的,通过3次定位,最后定位到紫色的那个元素,我们将中间一系列节点称为搜索路径
2)当插入一个新节点时,我们通过随机算法来定位新插入的节点有多少层
3、随机层数
最理想的是每一层的晋升概率都是50%
/* 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;
|
但是Redis的标准源码的晋升概率只有25%
4、插入过程
/* 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 = zsl->header; // 逐步降级寻找目标节点,得到「搜索路径」 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]; // 如果score相等,还需要比较value 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(); // 填充跨度 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++) { x->level[i].forward = update[i]->level[i].forward; update[i]->level[i].forward = x; /* update span covered by update[i] as x is inserted here */ 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++; } // 重排一下后向指针 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; } |
解析:首先我们在搜索合适插入点的过程中将「搜索路径」摸出来了,然后就创建新节点,创建的时候需要给这个节点随机分配一个层数,再将搜索路径上的节点和这个新节点通过前向后向指针串起来。如果分配的新节点的高度高于当前跳跃列表的最大高度,就需要更新一下跳跃列表的最大高度。
5、删除过程
首页找到搜索路径,然后对每一层相关节点排下向前向后指针,最后更新最高层maxLevel
6、更新过层
1)当我们调用zadd方法时,如果value不存在,则就是新增
2)如果value存在就是更新
3)如果该score值没有改变排序,那么位置就不需要调整,直接修改元素的score值
4)如果排序位置改变了,那就要调整位置了
5)调整位置源码如下
/* Remove and re-insert when score changes. */ if (score != curscore) { zskiplistNode *node; serverAssert(zslDelete(zs->zsl,curscore,ele,&node)); znode = zslInsert(zs->zsl,score,node->ele); /* We reused the node->ele SDS string, free the node now * since zslInsert created a new one. */ node->ele = NULL; zslFreeNode(node); /* Note that we did not removed the original element from * the hash table representing the sorted set, so we just * update the score. */ dictGetVal(de) = &znode->score; /* Update score ptr. */ *flags |= ZADD_UPDATED; } return 1; |
解析:通过上诉源码可以看出,更新时候,先找到value对应的score,然后删除,删除后在插入这个新的节点。
6、如果score值时相同处理逻辑
如果zset中的score值都相同,zset的排序首页比较score值,如果相同则比较value值。
7、元素排名(rank)规则
Redis的skiplist的forward指针上增加了span属性,表示从前一个节点沿着当前层的forward指针跳到当前这个节点中间会跳过多少的节点,Redis在插入删除操作时会更新span的大小,当我们要计算一个元素的排名时,只需要将span进行相加就能算出元素的最终的rank值
struct zslforward { zslnode* item; long span; // 跨度 } struct zsl { String value; double score; zslforward*[] forwards; // 多层连接指针 zslnode* backward; // 回溯指针 } |