就这一次,用20张图带你彻底击溃Redis跳表

上面的三类结构都存在一些问题,所以要进行改造,可以看到数组和平衡树的某些特性决定了它们不容易被改造(数组内存连续性、平衡树节点多指针和层级关系),相反链表最有潜力被改造优化。

在有序链表中插入和删除都比较简单,搜索时无法依靠下标只能遍历,但是明明知道要走两步可以到达目的地,偏偏只能一步步走,这就是痛点。

如图演示了O(n)遍历元素35和跳跃搜索元素35的过程:

就这一次,20张图带你彻底击溃Redis跳表

貌似看到了曙光,那么如何实现跳跃呢?

没错!给链表加索引,让索引告诉我们下一步该跳到哪里。

看到这里又让我想起来那个经典的中间层理论,遇到问题,试着加个中间层试试,或许就完美解决了。

跳跃链表的实现原理

=========

前面说了可以给普通链表加索引来解决,但是具体该怎么操作,以及其中有什么难点?一步步来分析。

在工程中对跳表索引层数和结点是否作为索引结点,是其很重要的属性,后面就详细讲一下,现在先看一种简单场景,说明索引带来的便利性。

简单的索引

=====

选择每隔1个结点为索引结点,并且索引为一层,虽然在工程中这种形式比较标准化,不过足以说明索引带来的加速。

可以将链表中的偶数序号节点增加一层指针,让其指向下一个偶数节点,如图所示:

就这一次,20张图带你彻底击溃Redis跳表

搜索过程:

假如要搜索值为55的节点,则先在上层进行搜索,由16跳到38,在38的下一跳将到达72,因此向下降一级继续类似的搜索,则找到55。

就这一次,20张图带你彻底击溃Redis跳表

多级索引

====

基于偶数节点增加索引并且只有两层的情况下,最高层的节点数是n/2,整体来看搜索的复杂度降低为O(n/2),并不要小看这个1/2的系数,看到这里会想 增加索引层数到k,那么复杂度将指数降低为O(n/2^k)。

索引层数不是无休止增加的,取决于该层索引的节点数量,如果该层的索引的节点数量等于2了,那么再往上加层也就没有意义了,画个图看一下:

就这一次,20张图带你彻底击溃Redis跳表

这个非常好理解,如果所在层索引结点只有1个,比如4层索引的结点16,只能顺着16向下遍历,无法向后跳到4层其他结点,因此当所在层索引结点数量等于2,则到达最高索引层,这个约束在分析跳表复杂度时很重要。

索引层数和索引结点密度

===========

跳表的复杂度和索引层数、索引结点的稀疏程度有很大关系。

索引层数我们从上面也看到了,稀疏程度相当于索引结点的数量比例,如果跳表的索引结点数量很少,那么将接近退化为普通链表,这种情况在数据量是较大时非常明显,画图看下(蓝色部分表示有很多结点):

就这一次,20张图带你彻底击溃Redis跳表

图中可以看到虽然有索引层,但是索引结点数量相对全部数据比例较低,这种情况下搜索35相比无索引情况优势并不明显。

所以跳表的效率和索引层数和索引结点的密度有密切的关系,当然索引结点太多也就等于没有索引了。

太少的索引结点和太多的索引结点都是一样的低效。

复杂度分析

=====

从前面的分析可知,跳表的复杂度和索引层数m以及索引结点间隙d有直接关系,其中索引结点间隙理解为相隔几个结点出现索引结点,体现了对应层索引结点的稀疏程度,在无索引结点时只能遍历无法跳跃。

如何确定最高索引层数m呢?

如果一个链表有 n 个结点,如果每两个结点取出一个结点建立索引,那么第一级索引的结点数是 n/2,第二级索引的结点数是n/4,以此类推第 m 级索引的结点数为 n/(2^m),前面说过最高层结点数为2,因此存在关系:

就这一次,20张图带你彻底击溃Redis跳表

算上最底层的原始链表,整个跳表的高度为h=logn(底数为2),每一层需要遍历的结点数是d,那么整个过程的复杂度为:O(d*logn)。

d表明了层间结点的稀疏程度,也就是每隔2个结点选取索引结点、或者每隔3个结点选取索引结点,每个4个结点选取索引结点…

最密集的情况下d=2,借用知乎某大佬的文章的图片:

就这一次,20张图带你彻底击溃Redis跳表

但是索引结点密集也意味着存储空间的增加,跳表相比较普通链表就是典型的用空间换时间的数据结构,这样就达到了AVL的复杂度O(logn)。

跳表的空间存储

=======

以d=2的最密集情况为例,计算跳表的索引结点总数:2+4+8+…n/8+n/4+n/2=n-2

就这一次,20张图带你彻底击溃Redis跳表

由等比数列求和公式得d=2的跳表额外空间为O(n-2)。

跳表的插入和删除

========

工程中的跳表并不严格要求索引层结点数量遵循2:1的关系,因为这种要求将导致插入和删除数据时的调整,成本很大.

跳表的每个插入的结点在插入时进行选择是否作为索引结点,如果作为索引结点则随机出层数,整个过程都是基于概率的,但是在大数据量时却能很好地解决索引层数和结点数的权衡。

我们针对插入和删除来看下基本的操作过程吧!

跳表元素17插入:

=========

链表的插入和删除是结合搜索过程完成的,贴一张William Pugh在论文中给出的在跳表中插入元素17的过程图(暂时忽略结点17是否作为索引结点以及索引层数,后面会详细说明):

就这一次,20张图带你彻底击溃Redis跳表

跳表元素1删除:

========

就这一次,20张图带你彻底击溃Redis跳表

跳表元素的删除与普通链表相比增加了索引层的判断,如果结点是非索引结点则正常处理,如果结点是索引结点那边需要进行索引层结点的处理。

跳跃链表的应用

=======

一般讨论查找问题时首先想到的是平衡树和哈希表,但是跳表这种数据结构也非常犀利,性能和实现复杂度都可以和红黑树媲美,甚至某些场景由于红黑树,从1990年被发明目前广泛应用于多种场景中,包括Redis、LevelDB等数据存储引擎中,后续将详细介绍。

跳表在Redis中的应用

============

ZSet结构同时包含一个字典和一个跳跃表,跳跃表按score从小到大保存所有集合元素。字典保存着从member到score的映射。这两种结构通过指针共享相同元素的member和score,不会浪费额外内存。

typedef struct zset {

dict *dict;

zskiplist *zsl;

} zset;

ZSet中的字典和跳表布局:

就这一次,20张图带你彻底击溃Redis跳表

ZSet中跳表的实现细节

============

随机层数的实现原理

=========

跳表是一个概率型的数据结构,元素的插入层数是随机指定的。Willam Pugh在论文中描述了它的计算过程如下:指定节点最大层数 MaxLevel,指定概率 p, 默认层数 lvl 为1

生成一个0~1的随机数r,如r<p,且lvl<MaxLevel ,则lvl ++

重复第 2 步,直至生成的r >p 为止,此时的 lvl 就是要插入的层数。

论文中生成随机层数的伪码:

就这一次,20张图带你彻底击溃Redis跳表

在Redis中对跳表的实现基本上也是遵循这个思想的,只不过有微小差异,看下Redis关于跳表层数的随机源码src/z_set.c:

/* 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.h中:

#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */

#define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */

可以看到while中的:

(random()&0xFFFF) < (ZSKIPLIST_P*0xFFFF)

第一眼看到这个公式,因为涉及位运算有些诧异,需要研究一下Antirez为什么使用位运算来这么写?

最开始的猜测是random()返回的是浮点数[0-1],于是乎在线找了个浮点数转二进制的工具,输入0.5看了下结果:

就这一次,20张图带你彻底击溃Redis跳表

可以看到0.5的32bit转换16进制结果为0x3f000000,如果与0xFFFF做与运算结果还是0,不符合预期。

我印象中C语言的math库好像并没有直接random函数,所以就去Redis源码中找找看,于是下载了3.2版本代码,也并没有找到random()的实现,不过找到了其他几个地方的应用:

  • random()在dict.c中的使用:

就这一次,20张图带你彻底击溃Redis跳表

  • random()在cluster.c中的使用:

就这一次,20张图带你彻底击溃Redis跳表

看到这里的取模运算,后知后觉地发现原以为random()是个[0-1]的浮点数,但是现在看来是uint32才对,这样Antirez的式子就好理解了。

ZSKIPLIST_P*0xFFFF

由于ZSKIPLIST_P=0.25,所以相当于0xFFFF右移2位变为0x3FFF,假设random()比较均匀,在进行0xFFFF高16位清零之后,低16位取值就落在0x0000-0xFFFF之间,这样while为真的概率只有1/4,更一般地说为真的概率为1/ZSKIPLIST_P。

对于随机层数的实现并不统一,重要的是随机数的生成,在LevelDB中对跳表层数的生成代码是这样的:

template <typename Key, typename Value>

int SkipList<Key, Value>::randomLevel() {

static const unsigned int kBranching = 4;

int height = 1;

while (height < kMaxLevel && ((::Next(rnd_) % kBranching) == 0)) {

height++;

}

assert(height > 0);

assert(height <= kMaxLevel);

return height;

}

uint32_t Next( uint32_t& seed) {

seed = seed & 0x7fffffffu;

if (seed == 0 || seed == 2147483647L) {

seed = 1;

}

static const uint32_t M = 2147483647L;

static const uint64_t A = 16807;

uint64_t product = seed * A;

seed = static_cast<uint32_t>((product >> 31) + (product & M));

if (seed > M) {

seed -= M;

}

return seed;

}

可以看到leveldb使用随机数与kBranching取模,如果值为0就增加一层,这样虽然没有使用浮点数,但是也实现了概率平衡。

跳表结点的平均层数

=========

我们很容易看出,产生越高的节点层数出现概率越低,无论如何层数总是满足幂次定律越大的数出现的概率越小。

如果某件事的发生频率和它的某个属性成幂关系,那么这个频率就可以称之为符合幂次定律。

幂次定律的表现是少数几个事件的发生频率占了整个发生频率的大部分, 而其余的大多数事件只占整个发生频率的一个小部分。

幂次定律应用到跳表的随机层数来说就是大部分的节点层数都是黄色部分,只有少数是绿色部分,并且概率很低。

定量的分析如下:

  • 节点层数至少为1,大于1的节点层数满足一个概率分布。
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

无论是哪家公司,都很重视基础,大厂更加重视技术的深度和广度,面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。

针对以上面试技术点,我在这里也做一些分享,希望能更好的帮助到大家。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
扫码获取!!(备注Java获取)**

img

最后

无论是哪家公司,都很重视基础,大厂更加重视技术的深度和广度,面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。

针对以上面试技术点,我在这里也做一些分享,希望能更好的帮助到大家。

[外链图片转存中…(img-6T9CiykJ-1713661542363)]

[外链图片转存中…(img-m90j8jF5-1713661542363)]

[外链图片转存中…(img-2xqsMC2H-1713661542364)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 21
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值