小白也能看懂的Redis教学基础篇——朋友面试被Skiplist跳跃表拦住了
各位看官大大们,双节快乐 !!!
这是本系列博客的第二篇,主要讲的是Redis基础数据结构中ZSet(有序集合)底层实现之一的Skiplist跳跃表。
不知道那些是Redis基础数据结构的看官们,可以翻阅我的上一篇文章:
小白也能看懂的REDIS教学基础篇——REDIS基础数据结构
今天我朋友突然找到我,说他面试被刷了。
我一脸吃惊,忙问到:怎么了,倒在什么题上了。
朋友说:面试官说,你说你了解Redis的基础数据结构,那我问问你,你知道什么是Skiplist跳跃表吗?讲讲它是一种什么样的数据结构。它有什么优势和缺陷,它是如何插入和删除的?
我:那你怎么回答的?
我朋友:我就说Redis不是只有五种基本数据结构 字符串(strings),列表(lists), 字典(dictht),集合(sets), 有序集合(ZSet)吗?然后人家就让我回家等通知了。
我:...
我朋友:怎么了,你怎么一副无语的表情。
我:哎,还是由我来给你科普一下吧。
Skiplist 跳跃表是跳表出自 William Pugh 于1989年发表的论文《Skip Lists: A Probabilistic Alternative toBalanced Trees 》。
在论文中 William Pugh 写到;
译文大意为:
跳跃表:平衡树的概率替代方案
跳跃列表是一种可以代替平衡树的数据结构。跳跃列表使用概率平衡,而不是严格的强制平衡,因此在跳跃列表中插入和删除的算法比平衡树的算法简单得多,速度也快得多。
注:平衡树(Balance Tree) 指的是,任意节点的子树的高度差都小于等于1。常见的符合平衡树的有,B树(多路平衡搜索树)、AVL树(二叉平衡搜索树)。
看到这里,看官们是不是一头雾水?先不要急,让我们来看看跳跃表的完整结构图。
看到这的看官是不有种想骂人的冲动?心里在想,这是个什么玩意,比平衡树还复杂。
//跳表
typedef struct zskiplist{
//头结点和尾节点的指针
struct skiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点层数
int level;
}; //跳表节点
typedef struct zskiplistNode{
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
};
- skiplistNode *header, *tail 指向首尾节点的指针。
- long length 表中节点的总数。
- int level 所有节点中层数最高的节点的层数。
- zskiplistNode *backward 后退指针,用来从尾部开始遍历到首节点。
- double score 分值,元素的排位分值。
- robj *obj 元素的对象指针。
- zskiplistNode *forward 前进指针指向该层下一个元素的指针。
- int span 跨度,用于记录两个节点之间的距离,跨度越大证明两个节点离得越远,在查找某个节点的过程中,将跨度累加,就是这个节点在跳跃表中的排位rank。
层是跳跃表节点的精髓和核心所在,跳跃表节点的level数组可以包含多个层元素。每个层元素都包含一个指向其他节点的指针,程序可以通过这些层来快速查找其他节点。一般来说,层数量越多,查找其他元素的速度就越快。
但是一个元素在插入时,他的层是怎么获得的呢?我们来看下面这个方法(此方法是仿照论文中的描述,用java实现的)。
/**
* 获取层级
* @param maxLevel 最大支持的层级数
* @return
*/
private int randomLevel(int maxLevel){
int lvl = 1;
/**
* 这里是关键 Math.random() > (0.5D) 等于true 的概率是 1/2
* 所以 lvl = 1 的概率是 1/2 lvl = 2 的概率 是 (1/2)*(1/2) = 1/4
* lvl = 3 的概率是 (1/2)*(1/2)*(1/2) = 1/8 从这里可以看出 lvl 越大概率越低
*/
for(;Math.random() > (0.5D) && lvl < maxLevel;){
lvl += 1;
}
return lvl;
}
从这个方法可以看出,对于每次新插入的元素,都要调用这个随机算法获得元素的层级。这里也正好对应了文章开头,论文中的话:跳跃列表使用概率平衡,而不是严格的强制平衡。
从概率上来说,期望的目标是分配到lv 1 是50%的概率,分配到lv 2 是百分之25%的概率,分配到lv 3 是12.5% 以此类推。Redis的跳跃表共有32层,可以容纳 2^32 个元素,在Redis标准源码中
元素的晋升几率只有25%,也就是上面代码中 0.5D 这个其实应该是 0.25D。所以Redis中的跳跃表更加扁平化,层高相对不高,这就带来一个问题,层高不高的话,跨度就小,查找元素需要遍历的次数也就相应的增加了。
查找元素:
跳越表的元素查找是从header 的第 zskiplist.level(表中层数最大的节点层数) 层开始遍历的。他先会判断最高层指向的下一个元素,是否是要找的元素,如果不是,判断是不是比要找的元素小,
如果比要找的元素小,就继续查找下一个。如果比要找的元素大,就向下走一个层级,比如一开始是lv 5,如果找到的元素比要找的元素大,就下降一个层级,到lv 4 继续找。以此类推,直到找到期望的元素为止。
现在我们要查找分数为85分的学生,查找路径如下图所示
插入元素:
创建一个新的元素节点。然后在调用 randomLevel 获取节点层级。
想要插入元素,就要先查找到所有元素中分值仅小于插入元素的分值的原数。比如要在上面的列表中插入一个分数为75分的学生二哈,就要先查找到分数仅小于75分的学生。在查找的时候还要记录下要更新的层级。如二哈这个节点拥有L4就要记录下据距他最近的L4,L3,L2,L1。
将新增节点每个层的前进指针连接到它对应的要更新层的前进指针指向的下一个节点。然后遍历要更新的层数组,断开这些层的前进指针,并将它连接到新增的几点上。这里基本和链表是一样的更新方式。最后更新后退节点。
连接更新完成后,看看新增节点的层级是否大于跳跃表中记录的节点最大层级高度,如果大于就将跳跃表的最大层级高度更新成新节点的层级高度。
删除元素:
删除节点过程和插入类似,都需要先把这个节点找出,然后对于每个相关节点重排一下向前向后指针,同时还要注意更新下跳跃表中记录的最大层级高度。
更新元素:
当我们调用ZSet的zadd方法时,如果该元素不存在,就执行正常的插入过程。如果元素已经存在了,如果要更新分值,则Redis会先删除原先的元素,在插入新的元素。如果不用更新分值,Redis会直接更新节点上的元素数据(这是在5.0以后的改动,之前的是不论跟不跟新都直接先删除在插入)。
参考书籍:
《Reids设计与实现》
《Redis深度历险——核心原理与应用实践》
创作不易,如果转载请注明出处,小编在此感谢各位看官。
小白也能看懂的Redis教学基础篇——朋友面试被Skiplist跳跃表拦住了的更多相关文章
- 小白也能看懂的Redis教学基础篇——做一个时间窗限流就是这么简单
不知道ZSet(有序集合)的看官们,可以翻阅我的上一篇文章: 小白也能看懂的REDIS教学基础篇--朋友面试被SKIPLIST跳跃表拦住了 书接上回,话说我朋友小A童鞋,终于面世通过加入了一家公司.这 ...
- 小白也能看懂的Redis教学基础篇——redis神秘的数据结构
各位看官大大们,周末好! 作为一个Java后端开发,要想获得比较可观的工资,Redis基本上是必会的(不要问我为什么知道,问就是被问过无数次).那么Redis是什么,它到底拥有什么神秘的力量,能获得众 ...
- 搭建分布式事务组件 seata 的Server 端和Client 端详解(小白都能看懂)
一,server 端的存储模式为:Server 端 存 储 模 式 (store-mode) 支 持 三 种 : file: ( 默 认 ) 单 机 模 式 , 全 局 事 务 会 话 信 息 内 存 ...
- 小白也能看懂的插件化DroidPlugin原理(二)-- 反射机制和Hook入门
前言:在上一篇博文<小白也能看懂的插件化DroidPlugin原理(一)-- 动态代理>中详细介绍了 DroidPlugin 原理中涉及到的动态代理模式,看完上篇博文后你就会发现原来动态代 ...
- 小白也能看懂的插件化DroidPlugin原理(三)-- 如何拦截startActivity方法
前言:在前两篇文章中分别介绍了动态代理.反射机制和Hook机制,如果对这些还不太了解的童鞋建议先去参考一下前两篇文章.经过了前面两篇文章的铺垫,终于可以玩点真刀实弹的了,本篇将会通过 Hook 掉 s ...
- 【vscode高级玩家】Visual Studio Code❤️安装教程(最新版🎉教程小白也能看懂!)
目录 如果您在浏览过程中发现文章内容有误,请点此链接查看该文章的完整纯净版 下载 Linux Mac OS 安装 运行安装程序 同意使用协议 选择附加任务 准备安装 开始安装 安装完成 如果您在浏览过 ...
- redis skiplist (跳跃表)
redis skiplist (跳跃表) 概述 redis skiplist 是有序的, 按照分值大小排序 节点中存储多个指向其他节点的指针 结构 zskiplist 结构 // 跳跃表 typede ...
- 小白也能看懂插件化DroidPlugin原理(一)-- 动态代理
前言:插件化在Android开发中的优点不言而喻,也有很多文章介绍插件化的优势,所以在此不再赘述.前一阵子在项目中用到 DroidPlugin 插件框架 ,近期准备投入生产环境时出现了一些小问题,所以 ...
- 小白也能看懂的插件化DroidPlugin原理(一)-- 动态代理
前言:插件化在Android开发中的优点不言而喻,也有很多文章介绍插件化的优势,所以在此不再赘述.前一阵子在项目中用到 DroidPlugin 插件框架 ,近期准备投入生产环境时出现了一些小问题,所以 ...
随机推荐
- Vue + axios + SpringBoot 2实现导出Excel
Vue + axios + SpringBoot 2实现导出Excel 1. 前端js代码-发送Http请求 /** * 文件下载 * @param url 下载地址 * @param fileNam ...
- 基于JSP+Servlet的学生信息管理系统
JavaWeb期末项目,一个基于JSP和Servlet的学生信息管理系统实现,前端用了bootstrap和一些自定义的css样式,数据库用了mysql 传送门: GitHub 实现功能 登录(教师, ...
- unity3d插入android有米广告
有米官网:http://www.youmi.net/register?r=MTI0MDg= 国内的广告,我觉得万普和有米还不错,我也只试了这两个,其他的都是看评价的,呵呵~~~首先我们去有米官网注册一 ...
- C#中SQL Server的几点注意事项
背景 在C#中处理sql会遇到一些奇怪的问题,在这里做一个小的总结,内容会随着经历不断积累. 内容 1.DataTime?和DataTime的区别. DataTime?定义的数据为可空类型,允许其为 ...
- 学习STM32的一些记录_创建库函数版本的工程
1.新建一个文件夹,用于存放MDK的工程所有文件.例如新建文件夹Template. 2.在Template下新建一个USER文件夹,用于存放工程. 3.打开MDK5,新建工程,目录在USER下. 4. ...
- 反射之hasattr() getattr() setattr() 函数
Python的hasattr() getattr() setattr() 函数使用方法详解 hasattr(object, name)判断object中有没有一个name字符串对应的方法或属性,返回B ...
- Dubbo系列之 (七)链路层那些事(1)
辅助链接 Dubbo系列之 (一)SPI扩展 Dubbo系列之 (二)Registry注册中心-注册(1) Dubbo系列之 (三)Registry注册中心-注册(2) Dubbo系列之 (四)服务订 ...
- Python 之 Django框架( Cookie和Session、Django中间件、AJAX、Django序列化)
12.4 Cookie和Session 12.41 cookie Cookie具体指的是一段小信息,它是服务器发送出来存储在浏览器上的一组组键值对,下次访问服务器时浏览器会自动携带这些键值对,以便服务 ...
- JVM运行时数据区--方法区
运行时数据区结构图(温习): 堆.栈.方法区的交互关系 方法区的理解 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域 方法区在JVM启动时就会被创建,并且它的实际的物理内存 ...
- 正则表达式与SQL
在我心中正则表达式和SQL就是一样的东西. SQL是结构化查询语言,是根据某个查询.修改规则来查询修改数据,是描述一个规则给数据库,数据库来执行, 数据库返回结果,过程不需要考虑,不算是编程语言. 正 ...