写在前
本系列文章已被收录到专栏中,如果各位看官有兴趣,可以移步深入理解Redis专栏
正文
话说,小张去参加了一场面试。。
面试官:小张我看你表现的不错啊,最后一个问题,回答的让我满意了我就让你过😎😎
小张:瑟瑟发抖.jpg
面试官:现在给你个场景题:有个某博大V,有1000w粉丝,我有时候会去按照用户id等等排序,你去设计一个数据结构维护这些数据。
小张:这当然是要用Redis中的zset存储了、😁
面试官:那你给我讲讲为啥🙄🙄
小张:行勒,
skipList,跳跃表, 是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。 跳表就是在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。
我们可以先思考一下数组和链表的优缺点:
- 数组查找可以用二分查找,时间复杂度O(logN),但是插入元素会导致后边的元素集体向后移动。
- 单链表查找只能从头节点向后查找,时间复杂度O(N),但是插入元素只需要改变前序节点的指针即可。
基于上述特点,人们提出了一个跳跃表的概念,节点分层,上层节点之间有跨度span,每个节点都有指向前边一个节点的指针backword(头节点和第一个节点除外)
面试官:(这小子有点东西)行了,那你给我讲讲他的实现细节吧。
小张:(这面试官真能抠,说着一道代码甩出 (′д` )…彡…彡),我们先从数据结构入手,跳表的数据结构相较而言比较简单,容易实现,在Redis源码中有写道:
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele; // 存储数据
double score; // 排行的分数
struct zskiplistNode *backward; // 前序节点
struct zskiplistLevel {
struct zskiplistNode *forward; // 下一个节点
unsigned long span; // 间距
} level[]; // 这是node的一个属性,代表这个node有几层,每一层都有详细的forward、span
} zskiplistNode;
这是跳表node的结构体,跟我们普通的node差不多,其中要额外注意的是:
backword
:只能指向当前节点最底层的前一个节点,头节点和第一个节点的backword
指向NULL,从后向前遍历跳跃表时使用。level[]
是一个柔性数组,在生成节点的时候会调用一个方法获取他的长度,这个我们之后再说。
除此之外,还需要定义一个跳跃表的结构体:
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 首尾指针
unsigned long length; // 跳表长度
int level; // 最大等级
} zskiplist;
我们可以看到在O(1)的时间复杂度下,能够快速获取到跳跃表的头节点、尾节点、长度和高度。
一个个节点连接起来,就能构成我们前边的那个完整的跳跃表,这样子查找元素先从最高level查询,总的时间复杂度就会是O(logN)。
面试官:不错,那么说一说跳跃表节点level层高是怎么计算的吧
小张: Redis中设置了最底层高是1,最大值是一个常量**ZSKIPLIST_MAXLEVEL
,这个值在Redis 5以前是32**,Redis 5的时候是64
Redis 6又改为了32。
为啥一直变呢?我在这个里找到了答案 github.com/redis/redis…
然后我们来看一下层数计算的方法:
/* 为我们将要创建的新跳过列表节点返回一个随机级别。
* 此函数的返回值介于 1 和 ZSKIPLIST_MAXLEVEL
*(包括两者)之间,具有类似幂律的分布,其中
* 级别越高,返回的可能性就越小。 */
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) // ZSKIPLIST_P=0.25
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
要注意的是 ZSKIPLIST_P
表示的意思是当前节点出现在下一层的概率,代码中设置的是1/4
level的初始值为1,通过while循环,每次生成一个随机值,取这个值的低16位作为x,当x小于0.25倍的0xFFFF时,level的值加1;否则退出while循环。最终返回level和ZSKIPLIST_MAXLEVEL两者中的最小值。
面试官:我来抠一抠,跳跃表的首节点有什么特点?
小张:跳跃表的首节点是一个空节点,看代码最后创建了一个 满层数,score=0,ele=null的节点,用来管理各个层数的节点。
/* Create a new skiplist. */
zskiplist *zslCreate(void) {
zskiplist *zsl;
...
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); // 设置头节点,这里把MAXLEVEL传进去了
...
}
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
zskiplistNode *zn =
zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
zn->score = score; // 0,跳跃表中保证score都是整数
zn->ele = ele; // null
return zn;
}
面试官:(这小子基础竟然如此扎实),我再问你最后一个问题:zset底层除了跳跃表实现还有什么??
小张:zset还有另一种实现叫做压缩列表,在插入第一个元素的时候,redis会进行判断
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr)) // 插入的字符串长度
{
zobj = createZsetObject(); // 跳跃表
} else {
zobj = createZsetZiplistObject(); // 压缩列表
}
当然,当zset中元素个数大于zset_max_ziplist_entries
,或者插入的字符串长度大于zset_max_ziplist_value
的时候会转换成跳跃表,此操作不可逆
面试官:小伙子可以的,期待接下来的面试。
小张:太难了,差点就说不上来了,我得回去补一补,争取吊打面试官。
Over~
参考资料
- Redis源码5.0
- 跳跃表 —— 维基百科
- Set ZSKIPLIST_MAXLEVEL to 32
- 《Redis 5设计与源码分析》——陈雷著
end~ 🌹🌹🌹🌹