- Redis 的 zset 是一个复合结构,一方面它需要一个 hash 结构来存储 value 和 score 的 对应关系,另一方面需要提供按照 score 来排序的功能,还需要能够指定 score 的范围来获 取 value 列表的功能,这就需要另外一个结构「跳跃列表」。
- zset 的内部实现是一个 hash 字典加一个跳跃列表 (skiplist)。
- Redis 的跳跃表共有 64 层,意味着最 多可以容纳 2^64 次方个元素。每一个 kv 块对应的结构如下面的代码中的 zslnode 结构,kv header 也是这个结构,只不过 value 字段是 null 值——无效的,score 是 Double.MIN_VALUE,用来垫底的。
- kv 之间使用指针串起来形成了双向链表结构,它们是 有序 排列的,从小到大。
- 不同的 kv 层高可能不一样,层数越高的 kv 越少。同一层的 kv 会使用指针串起来。每一个层元素的遍历都是从 kv header 出发。
struct zslnode {
string value;
double score;
zslnode*[] forwards; // 多层连接指针
zslnode* backward; // 回溯指针
}
struct zsl {
zslnode* header; // 跳跃列表头指针
int maxLevel; // 跳跃列表当前的最高层
map<string, zslnode*> ht; // hash 结构的所有键值对
}
- 跳跃列表查找算法复杂度O(lg(n))。
- 要定位到那个紫色的 kv,需要从 header 的最高层开始遍历找到第一个 节点 (最后一个比「我」小的元素),然后从这个节点开始降一层再遍历找到第二个节点 (最 后一个比「我」小的元素),然后一直降到最底层进行遍历就找到了期望的节点 (最底层的最 后一个比我「小」的元素)。
- 将中间经过的一系列节点称之为「搜索路径」,它是从最高层一直到最底层的每一 层最后一个比「我」小的元素节点列表。
- 对于每一个新插入的节点,都需要调用一个随机算法给它分配一个合理的层数。
- 官方的跳跃列表更加的扁平化,层高相对较低,在单个层上需要遍历的节点数量会稍多一 点。
- 跳跃列表 会记录一下当前的最高层数 maxLevel,遍历时从这个 maxLevel 开始遍历性能就会提高很 多。
- 首先我们在搜索合适插入点的过程中将「搜索路径」摸出来了
- 然后就可以开始创建新 节点了,创建的时候需要给这个节点随机分配一个层数,再将搜索路径上的节点和这个新节 点通过前向后向指针串起来
- 如果分配的新节点的高度高于当前跳跃列表的最大高度,就需 要更新一下跳跃列表的最大高度
- 删除过程和插入过程类似,都需先把这个「搜索路径」找出来。然后对于每个层的相关 节点都重排一下前向后向指针就可以了。同时还要注意更新一下最高层数 maxLevel。
- 如果这个 value 已经存在了,只是调整一下 score 的值,那就需要走一个更新的流程。假设这个新的 score 值不会带来排序位置上的改变,那么就不需要调整位置,直接修改元素的 score 值就 可以了
- 如果排序位置变了,Redis是先删除这个元素,再插入这个元素
- zset 的排序元素不只看 score 值,如果 score 值相同还需要再比较 value 值 (字符串比较)
- zset 可以获取元素的排 名 rank
- 那这个 rank 是如何算出来的?
- Redis 在 skiplist 的 forward 指针上进行了优化,给每一个 forward 指针都增加了 span 属 性,span 是「跨度」的意思,表示从前一个节点沿着当前层的 forward 指针跳到当前这个节 点中间会跳过多少个节点。Redis 在插入删除操作时会小心翼翼地更新 span 值的大小。
- 当我们要计算一个元素的排名时,只需要将「搜索路径」上的经过的所有节点的跨 度 span 值进行叠加就可以算出元素的最终 rank 值。