【愚公系列】2021年11月 C#版 数据结构与算法解析 Redis有序集合zset实现原理(跳表)

Redis有序集合中的元素的编码可以是 ziplist 或者 skiplist。ziplist和skiplist编码选择的标准在于Redis里的元素的数量以及元素成员的长度。当满足以下2个条件时,元素编码为ziplist:
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素成员的长度小于64字节
ziplist:
ziplist编码的有序集合对象使用压缩列表作为底层实现。每个集合使用2个紧挨在一起的压缩列表节点来保存,第一个保存元素的成员,第二个保存元素的分值。压缩列表内的集合按分值从小到大排序,分值较小的元素被放置在靠近表头的位置,分值较大的元素在靠近表尾的位置。
skiplist:
跳跃表是一种有序的数据结构,它通过在每个节点上维持多个指向其它节点的指针来达到快速访问的目的。跳跃表在插入、删除和查找操作上的平均复杂度为O(logN),最坏为O(N),可以和红黑树相媲美,但是在实现起来,比红黑树简单很多。在Redis中的zset就是使用了跳跃表的实现有序集合。 具有如下性质:
- 由很多层结构组成;
- 每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点;
- 最底层的链表包含了所有的元素;
- 如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);
- 链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;

如上图所示,有一个链表保存着若干有序的数字,如果我们想找到 <16,23,25,31>这几个数字的话,简单的方法就是遍历一遍,时间复杂度为O(N)。数据量小的话还好,但是数据量一旦十分庞大的话就非常耗时了。这个时候就是需要我们去优化的了,找一个线性的顺序表,我们一般使用二分法查找,时间复杂度为O(LogN)。但是链表不适用二分法去查找(链表没有线性表那种连续下标,无法确定中间位置)。于是跳表产生了,它是一种随机化的数据结构,类似二叉搜索树,我们把一些节点提取出来作为索引。如下:>这几个数字的话,简单的方法就是遍历一遍,时间复杂度为O(N)。数据量小的话还好,但是数据量一旦十分庞大的话就非常耗时了。这个时候就是需要我们去优化的了,找一个线性的顺序表,我们一般使用二分法查找,时间复杂度为O(LogN)。但是链表不适用二分法去查找(链表没有线性表那种连续下标,无法确定中间位置)。于是跳表产生了,它是一种随机化的数据结构,类似二叉搜索树,我们把一些节点提取出来作为索引。如下:

我们提取了<13,19,25,34>作为一级索引,这样可以减少比较的次数。同样的,有一级索引就可以建立二级索引,如下,在海量的数据情况下这种方式无疑会大大减少查询的时间O(logN)

Redis中跳跃表的结构 在上面简单了解了跳跃表的基本含义及实现思想。在redis中,跳跃表的定义在 server.h 中,包含2个结构体,分别表示跳跃表节点的zskiplistNode和跳跃表zskilist。zskiplistNode 结构用于表示跳跃表节点,zskiplist结构用来保存跳跃表节点的相关信息。源码如下:
typedef struct zskiplistNode {
//节点对应成员的对象,一般是SDS
robj *obj;
//分数,跳跃表的顺序按照分值进行排列
double score;
//存储后继节点的指针
struct zskiplistNode *backward;
// 层级
struct zskiplistLevel {
// 存储前驱节点的指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
// 跳跃表的表头节点和表尾节点
struct zskiplistNode *header, *tail;