这里写目录标题

  • ziplist
    • 压缩列表结构
      • 压缩列表结构
      • 压缩列表节点结构
    • 连锁更新
    • 压缩列表在Redis中的用途
  • skiplist
    • 传统跳表
    • 改进后的跳表
    • zset中的跳表
    • redis中如何保证skiplist的查找性能
    • skiplist与哈希表、平衡树、B+树的比较
      • skiplist与哈希表比较
      • skiplist与平衡树的比较
      • skiplist与B+树的比较

有序集合对象ZSet的编码可以是ziplist(压缩列表)或者skiplist(跳表)。同时满足以下条件时使用ziplist编码:

  1. 元素数量小于128个。
  2. 所有member的长度都小于64字节。

以上两个条件的上限值可通过zset-max-ziplist-entries和zset-max-ziplist-value来修改。不满足这两个条件时使用一个dict + 一个skiplist来实现的。简单来讲,dict用来查询数据到分数的对应关系,而skiplist用来根据分数查询数据(可能是范围查找)

ziplist

ziplist编码的有序集合使用紧挨在一起的压缩列表节点来保存,ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。

一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

压缩列表结构

压缩列表结构

参数说明:
zlbytes:记录 ziplist 整个结构体的占用空间大小。
zltail:记录整个 ziplist 中最后一个 entry 的偏移量。
zllen:记录了 ziplist 包含的entry数量。
entryN:ziplist 的节点。
zlend:特殊值0xFF(十进制255),用于标记 ziplist 的末端。

压缩列表节点结构

参数说明:
previous_entry_length:记录压缩列表中前一个节点的长度。previous_entry_length属性的长度可以是1字节或者5字节:如果前一节点的长度小于 254 字节,那么previous_entry_length属性的长度为1字节。如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节。
encoding:记录节点所保存数据的类型以及长度。
contents:节点所保存数据的值。

连锁更新

由于压缩列表的previous_entry_length属性可能是1字节或5字节,若在一个压缩列表中,有多个连续的、长度介于250字节到253字节之间的节点,则添加新节点或删除节点都有可能会引发多个节点的连续多次空间扩展,这种现象称之为“连锁更新”。

压缩列表在Redis中的用途

  1. 作为list的底层实现之一:当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用ziplist来做list的底层实现。
  2. 作为hash的底层实现之一:当一个哈希键只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用ziplist来做hash的底层实现。
  3. 作为有zset的底层实现之一:如果一个有序集合包含的元素数量比较少且有序集合中元素的成员是比较短的字符串时,Redis就会使用ziplist来作为zset的底层实现。

skiplist

skiplist编码的有序集合底层是一个命名为zset的结构体,而一个zset结构同时包含一个字典和一个跳跃表。跳跃表按score从小到大保存所有集合元素。而字典则保存着从member到score的映射,这样就可以用O(1)的复杂度来查找member对应的score值。虽然同时使用两种结构,但它们会通过指针来共享相同元素的member和score,因此不会浪费额外的内存。

跳表(skip List)是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度。

传统跳表

改进后的跳表

在传统的跳表上,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。删除数据也有同样的问题。

skiplist为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是3,那么就把它链入到第1层到第3层这三层链表中。下图展示了如何通过一步步的插入操作从而形成一个skiplist的过程:

刚刚创建的这个skiplist总共包含4层链表,现在假设我们在它里面依然查找23,下图给出了查找路径:

zset中的跳表

Redis的跳表由zskiplistNode和skiplist两个结构定义,其中 zskiplistNode结构用于表示跳跃表节点,而skiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等。Redis中的跳表整体结构如下所示:

上图左边是skiplist结构,该结构包含以下属性:

  1. header:指向跳跃表的表头节点,通过这个指针程序定位表头节点的时间复杂度就为O(1)。
  2. tail:指向跳跃表的表尾节点,通过这个指针程序定位表尾节点的时间复杂度就为O(1)。
  3. level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内),通过这个属性可以再O(1)的时间复杂度内获取层高最高的节点的层数。
  4. length:记录跳跃表的长度,即跳表目前包含节点的数量(表头节点不计算在内),通过这个属性,程序可以再O(1)的时间复杂度内返回跳跃表的长度。

上图右边是四个zskiplistNode结构,该结构包含以下属性:

  1. 层(level):节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离(跨度越大、距离越远)。在上图中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。每次创建一个新跳跃表节点的时候,程序都根据幂次定律(powerlaw,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。
  2. 后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。与前进指针所不同的是每个节点只有一个后退指针,因此每次只能后退一个节点。
  3. 分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
  4. 成员对象(oj): 各个节点中的o1、o2和o3是节点所保存的成员对象。

在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)。

redis中如何保证skiplist的查找性能

节点插入时随机出一个层数,仅仅依靠这样一个简单的随机数操作而构建出来的多层链表结构,能保证它有一个良好的查找性能吗?为了回答这个疑问,我们需要分析skiplist的统计性能。

执行插入操作时计算随机数的过程十分关键,这并不是一个普通的服从均匀分布的随机数,它的计算过程如下:

  1. 首先,每个节点肯定都有第1层指针(每个节点都在第1层链表里)。
  2. 如果一个节点有第i层(i>=1)指针(即节点已经在第1层到第i层链表中),那么它有第(i+1)层指针的概率为p(即上浮一层的概率为p)
  3. 节点最大的层数不允许超过一个最大值,记为MaxLevel。

这个计算随机层数的伪码如下所示:

randomLevel()level := 1// random()返回一个[0...1)的随机数while random() < p and level < MaxLevel dolevel := level + 1return level

randomLevel()的伪码中包含两个参数,一个是p,一个是MaxLevel。在Redis的skiplist实现中,这两个参数的取值为:

p = 1/4
MaxLevel = 32

即redis跳表使用1/4的概率随机上浮一层,redis跳表的最大层数为32层

skiplist与哈希表、平衡树、B+树的比较

skiplist与哈希表比较

  1. skiplist的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
  2. 查找单个key,skiplist的时间复杂度都为O(logn);而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。因此zset底层不仅使用了skiplist,也使用了hash表。

skiplist与平衡树的比较

  1. 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
  2. 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。

skiplist与B+树的比较

  1. redis 是内存数据库,进行读写数据都是操作内存,跟磁盘没啥关系,因此也不存在磁盘IO了,所以层高就不再是跳表的劣势了
  2. 跳表插入数据时的操作比较简单,只需要随机一下,就知道自己要不要往上加索引,根本不用考虑前后结点的感受,也就少了旋转平衡的开销。而B+树是有一系列合并拆分操作的,换成红黑树或者其他AVL树的话也是各种旋转,目的也是为了保持树的平衡

有序集合对象 ZSet 的底层原理相关推荐

  1. java,从入土到出棺——2.数据结构(从容器(集合等)到底层原理)

    java,从入土到出棺--2.数据结构(从容器(集合等)到底层原理) 1 数据结构 1.1 概述 1.2 数据结构的种类 1.2.1 数组(Array) 1.2.2 栈(Stack) 1.2.3 队列 ...

  2. Java中Set集合的使用和底层原理

    文章目录 Set系列集合介绍 Set集合概述 HashSet无序原理 Set集合对象去重 LinkedHashSet TreeSet排序规则 Set系列集合介绍 Set集合概述 Set系列集合特点: ...

  3. 有序集合使用与内部实现原理

    有序集合类型 (Sorted Set) 相比于集合类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值.有序集 ...

  4. iOS之深入解析对象isa的底层原理

    对象本质 一.NSObject 本质 OC代码的底层实现实质是 C/C++代码 ,继而编译成汇编代码,最终变成机器语言. ① clang C/C++ 编译器 Clang 是⼀个 C 语⾔.C++.Ob ...

  5. redis的zset的底层实现_Redis(三)--- Redis的五大数据类型的底层实现

    1.简介 Redis的五大数据类型也称五大数据对象:前面介绍过6大数据结构,Redis并没有直接使用这些结构来实现键值对数据库,而是使用这些结构构建了一个对象系统redisObject:这个对象系统包 ...

  6. Redis 动态字符串(SDS)底层原理详解

    文章目录 前言 1. 动态字符串 1.1 SDS的数据结构 1.2 SDS 与 C 字符串的区别 1.2.1 常数复杂度获取字符串长度 1.2.2 杜绝缓冲区溢出 1.3 减少修改字符串时带来的内存重 ...

  7. 底层实现_Redis有序集合zset的底层实现

    1. 编码 zset的编码有ziplist和skiplist两种. 底层分别使用ziplist(压缩链表)和skiplist(跳表)实现. 什么时候使用ziplist什么时候使用skiplist? 当 ...

  8. redis的zset的底层实现_Redis中hash、set、zset有多牛?从底层告诉你数据结构原理...

    前言 今天来说下Redis中hash.set.zset的底层数据结构原理! Redis-哈希对象(hash) hash的底层存储有两种数据结构,一种是ziplist,另外一种是hashtable,这两 ...

  9. redis缓存数据库中zset数据结构底层算法实现原理:ziplist 和 skiplist

    有序集合对象是有序的.与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据. ①.编码 有序集合的编码可以是 ziplist 或者 skiplist. zipl ...

最新文章

  1. 最后2周 | 高级转录组分析和R语言数据可视化第十一期 (报名线上课还可免费参加线下课)...
  2. 【Android 高性能音频】Oboe 开发流程 ( Oboe 完整代码示例 )
  3. Java项目构建基础:统一结果,统一异常,统一日志
  4. python 模块 包 库_模块(包、库)
  5. 一个成功的研发团队应具备的9大属性
  6. C/C++:Windows编程—MFC基于CWnd自绘CListCtrl控件且带滚动条
  7. ssh (安全外壳协议)Secure Shell 百度百科
  8. Logistic回归分类算法原理分析与代码实现
  9. 【零开始】怎样购买、配置服务器及发布网站(页)?
  10. 微分方程Gear方法MATLAB,matlab求解常微分方程(组)
  11. 春运火车票抢票浏览器强力推荐,秒抢车票到手
  12. 网站漏洞修复之UEditor漏洞 任意文件上传漏洞
  13. centos7 yum 配置阿里云镜像
  14. bitbake hello world demo 实验
  15. Windows命令-解压缩文件-tar
  16. 三国论(11-15章)
  17. 查看Oracle数据库命中率
  18. IBM-LSF-社区版搭建记录
  19. 新型多功能、高性能量子点,可以用于医学成像、量子计算
  20. 关于《创业创新执行力》课程中邀请同学参与调查的“无效问卷”补救办法

热门文章

  1. 简单理解支付宝和蚂蚁花呗的架构
  2. 计算机网络-3数据链路层
  3. 迷你图+创建数据图表
  4. Qt QML 自绘GPS方位校北仪控件
  5. 习题10-7 十进制转换二进制
  6. 怎样解决AirPods无法连接或充电问题?
  7. 【云驻共创】华为云云原生之Istio控制面架构深度剖析
  8. python动画精灵梦叶罗丽_叶罗丽第八季20话,冰公主首次登场,深蓝双眸太美了...
  9. i9跑mysql_跑BWA比对测试一下酷睿I9的CPU
  10. Python源码剖析[19] —— 执行引擎之一般表达式(2)