首页 > 数据库 >Redis数据结构跳跃列表(skipList)与压缩列表(ziplist)

Redis数据结构跳跃列表(skipList)与压缩列表(ziplist)

时间:2024-09-19 11:27:05浏览次数:1  
标签:字节 ziplist 元素 Redis 列表 链表 查找 entryX

skiplist介绍

跳表是一种数据结构,它使得包含了n个元素的有序序列的查找和插入的平均时间复杂度都是O(logn),优于数组的O(n)复杂度,快速的查找是通过维护多层次的链表实现的,且与前一层(下面一层)链表的数量相比,每一层的链表元素数量更少
简单来讲跳表就是基于链表实现的有序列表,通过维护一个多层级的链表实现了快速查找的效果,且插入跟删除效率也特别高

为何引入跳表

常见的在一个特定数据集查找指定数据,有一些几种算法

  • 有序数组,插入时可以先对数组排序,查询时可以根据二分法降低查找的复杂度,缺点是插入和删除时,为了维护保持元素的有序性,需要进行大量元素的移动操作
  • 二叉查找树,既支持高效的二分查找算法,又能快速的插入和删除,但在某些极端条件下,二叉查找树有可能会变成线性链表,即退化为链表结构
  • 平衡二叉树,基于二叉查找树的优点,对其缺点进行了改进,引入了平衡二叉树,根据平衡算法的实现具体实现有AVL树、B树(B-Tree)、B+树(B+Tree)、红黑树,但是平衡算法的实现比较复杂且难理解
    跳表同样支持高效的查找,并且插入和删除数据的时间复杂度能和平衡二叉树媲美,最重要的是跳表的实现比平衡二叉树简单,

跳表的实现

由于跳表是基于有序链表,所以删除和添加的时候都需要先查询元素,先来看链表的查找逻辑,比如这里有个有序链表-10,8,15,65,86,98,99,在这里面指定数据65,只能从头沿着链表依次比较查找

链表不能像数组那样根据索引访问元素,只能沿着指针依次访问,所以不能使用二分查找进行查询,这个时候如果在原先列表上面加入一层,如果可以实现跳过一些列表查询,就可以提高查询效率了

在上面一层链表中,都会保存指向下个节点的指针,这样我们就可以跳过某些节点进行查找,在数据量比较大的时候,如果层级还是两个层级,效率可能提高的不多,所以可以适量增加层级,效率会直接翻倍,这里最下面一层链表会存储所有的元素,所以新维护的链表只是保存了下一节点的指针数据,在数据量大的情况下,这点空间可以忽略不记,以下是两个层级的跳跃列表

当进行查询元素92的时候,会首先查询第2层也就是最上面一层,

如果查找元素比目标元素还有大,则进行链表下移,不需要遍历当面连边的下面元素,从下一层级开始查找

如果查找元素比右边的元素还有大,则会往右遍历,如果比右边的小,则会从下一层查找


可以看到查找的时候速度是很快的,下面是插入元素144的过程,首先的查找,找到中间位置,并且修改左右元素的指针,

但是棘手的问题来了,也是跳表这个结构查找速度快的前提,插入元素的时候如何判断是否在上面层级插入元素,如果是两个层级中间节点的数量严格按照比例来分配的话,也不现实,因为插入或删除元素的话当前节点以后的节点都得重新调整一遍,这样虽然查询没受影响,但是插入和删除效率会低很多,所以这里的层级是通过随机数生成的,也不是那么的随机,存在概率的问题,这个是论文中计算层级的代码:

如果节点在第i层出现,在第i+1层中出现的概率为p,同时跳表有最大层数限制MaxLevel,而在redis中这个结构p为1/4,最大层级为32,random为0-1之间的数

public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            int level = 1;
            Random random = new Random();
            while (random.nextFloat() < 0.25f && level < 64) {
                level += 1;
            }
            System.out.println(level);
        }
    }

经过测试发现level = 1的概率还是比较大的,产生越高节点层数,概率越低,p为1/4,所以这个skiplist相对来说比较扁平

  • level=1的概率为1-p
  • level>=2的概率为p
  • level=2的概率为p(1-p)

    上图为节点的平均指针数,因为skiplist的空间换时间的结构
    通过以上可以总结中skiplist的几个特性:
  1. skiplist包含多个层级每层称为一个level,level从0开始递增
  2. skiplist0层,也就是最底层,应该包含所有的元素
  3. 每个level是一个有序链表
  4. level小的层包含level大的层,如果第i层有这个元素,那么第i-1层都有这个元素

Redis中跳表的实现

还得从redis的数据结构说起,众所周知redis的sorted set类型也就是zset,可以进行zadd,zrange,zrangebyscore,并且里面的元素是有序的,去重的,可以从小到大,也可以从大到小
首先来看redis中的实现

obj是当前key的对象。如果为null,需要新建,在新建的过程中会根据redis.conf里面的内容,如果配置文件zset-max-ziplist-entries为0或者当前需要插入的元素大小(序列化之后的字符串长度)超过zset-max-ziplist-value则用zset,否则用ziplist
zset-max-ziplist-entries表示ziplist最大的元素个数,默认值为128
zset-max-ziplist-value表示单个元素的最大值的阈值,单位为字节,默认值为64

可以看出Redis中Sorted Set有两种实现方式,一种是直接创建的zset数据类型,另一种是直接使用ziplist,第一种直接创建zset的时候还创建了一个字段dict,同时创建了skiplist

下面是zset以及skiplist、skiplistnode的数据类型

对象创建完了然后看zsetAdd方法,里面的实现逻辑同样区分是skiplist还是ziplist,这里先主要看skiplist的实现逻辑

首先是根据dictFind查找是否存在元素,但是这个查找不是从skiplist里面查找的,而是从dict里面查找的,如果不存在这个元素,并且选择不是xx,才会选择插入新的元素到skiplist
插入逻辑中首先插入skiplist,然后插入dict,首先看zslInsert

首先会查找每个层级需要插入的位置,根据score大小或者,然后生成随机的level,然后更新插入的每个层级的前进指针(forward)与后退指针(backward)及跨度span,这个累加的span就是rank排名

ziplist

redis的zset在长度较小时采用的是压缩列表而不是跳跃列表,链表是一个个指针指向,内存不连续而且指针还要占用额外的内存,由此引出了ziplist,是由一系列特殊编码的连续内存块组成,可以在任意一端进行压入和弹出操作,
所以ziplist是一种特殊的“双端链表”,结构如下:

zlbytes:记录总的字节数,占用4个字节
zltail:尾偏移量,通过这个可以直接获取尾结点的地址,占用4个字节
zllen:entery的长度,占用2个字节
entry:具体数据,长度由内容确定
zlend:结束标志,固定值:0xff,占用1个字节
ziplist可以看做特殊的双端链表是因为ziplistentry不像普通的链表那样记录前后指针,因为记录两个指针需要占用16个字节,浪费内存,所以使用下面的结构

previous_entry_length:前一节点的长度,占用1个或者5个字节(如果前一节点的长度小于254字节,则用1个字节表示长度,如果前一节点大于254个字节,则用5个字节表示)
encoding:编码属性,记录content的数据类型(字符串或者整数)以及长度,占用1个,2个或者5个字节
content: 负责保存节点的数据,可以是字符串或整数

ziplist中的寻址方式:
正序寻址:当前元素加上自己的Previous_entry_length+encoding+content的字节长度,就是下一个元素的地址
倒序寻址:当前元素减去Previous_entry_length的前一节点的长度,就是上一个元素的地址

ziplist的连锁更新:

当删除压缩列表zl1位置p1的元素entryX,或者在压缩列表zl2的p2位置插入元素entryY会发生什么了?
zl1:由于entryx为128字节,所以entryX+1的previous_entry_length为1个字节,entryX+1为253字节,所以entryX+2的previous_entry_length为1个字节,当删除entryX的时候,元素entryX+1的前一个节点就会变成元素entryX-1,长度为512字节,此时entryX+1的previous_entry_length需要5个字节才能存储entryX-1的长度,则元素entryX+1扩至157字节,而由于entryX+1的长度改变,所以entryX+2的previous_entry_length需要改变,以此类推,由于删除了entryX,导致后面的entryX+1、entryX+2的长度都必须要扩展,而每次扩展都会内存重新分配,效率是很低的,在zl2中插入元素也是同样的道理

参考文档
https://www.cl.cam.ac.uk/teaching/2005/Algorithms/skiplists.pdf
https://redis.io/glossary/redis-ziplist/

标签:字节,ziplist,元素,Redis,列表,链表,查找,entryX
From: https://www.cnblogs.com/LiuFqiang/p/18396067

相关文章

  • Redis大key有什么危害?如何排查和处理?
    目录标题什么是bigkey?bigkey是怎么产生的?有什么危害?如何发现bigkey?1、使用Redis自带的--bigkeys参数来查找。2、使用Redis自带的SCAN命令3、借助开源工具分析RDB文件4、借助公有云的Redis分析服务如何处理bigkey?这个问题在面试中还是比较容易遇到的,......
  • Redis基础数据结构之 quicklist 和 listpack 源码解读
    目录标题quicklist为什么要设计quicklist?quicklist特点ziplistquicklist数据结构listpacklistpack是什么?listpack数据结构ziplist干啥去了?为什么有listpack?什么是ziplist的连锁更新?listpack如何避免连锁更新?listpack替代了quicklist吗?quicklist为什么要设计qu......
  • Redis 突然变慢了如何排查并解决?
    当Redis突然变慢时,可以通过一系列步骤来排查并解决问题。以下是一个详细的排查和解决流程:1.监控Redis性能指标使用Redis自带的工具:如redis-cli工具,通过执行INFO命令来查看Redis的关键性能指标,如内存占用情况、命令执行时间、连接数等。使用监控工具:如RedisInsight等,这些工具能提供......
  • 2024Mysql And Redis基础与进阶操作系列(2)作者——LJS[含MySQL登录;DDL;DML;举例说明;编码
    目录1.MySQL的登录1.1服务的启动和停止方式1:使用图形界面工具步骤1:打开windows服务 步骤2:找到MySQL80(点击鼠标右键)→启动或停止(点击)编辑补充说明2点:1.2自带客户端的登录与退出登录方式1:MySQL自带客户端注意:退出登录2MySQL数据库基本操作-DDL和DML2.1.DDL解释2.......
  • C++ vector 列表初始化
    vector<int>vl(10);//v1有10个元素,每个的值都是0vector<int>v2{10};//v2有1个元素,该元素的值是10vector<int>v3(10,1);//v3有10个元素,每个的值都是1vector<int>v4{10,1};//v4有2个元素,值分别是10和1如果初始化时使用了花括号的形式但是提供的值又不能......
  • PowerShell 命令来备份 Windows 10 的服务列表:CMD 批处理命令来备份 Windows 10 的服
    PowerShell命令来备份Windows10的服务列表:powershellCopyCodeGet-Service|Export-Csv-Path"C:\ServiceListBackup.csv"-NoTypeInformation这条命令会将所有服务信息导出到C:\ServiceListBackup.csv文件中。确保您有写入该路径的权限。CMD批处理命令来备份Windo......
  • Redis 字典的哈希函数和 rehash 操作详解
    Redis字典的哈希函数和rehash操作详解在Redis中,字典(HashTable)是一种重要的数据结构,用于存储键值对。下面解释Redis字典的哈希函数和rehash操作。一、哈希函数作用Redis的字典使用哈希函数将键转换为一个整数索引,这个索引用于确定键值对在哈希表中的存储位......
  • Python高手之路:揭秘列表的高级操作技巧
    引言列表的高级操作不仅能够提升代码的可读性和执行效率,还能让我们的程序更加灵活多变。无论是在日常开发还是数据分析任务中,掌握这些技巧都将使你如虎添翼。接下来,让我们从最基础的概念出发,一步步深入了解列表的高级操作吧!基础语法介绍首先,我们需要明确几个核心概念:列表推导......
  • Redis存储原理
    前言    我们从redis服务谈起,redis是单reactor,命令在redis-server线程处理。还有若干读写IO线程负责IO操作(redis6.0之后,Redis之pipeline与事务)。此外还有一个内存池线程负责内存管理、一个后台文件线程负责大文件的关闭、一个异步刷盘线程负责持久化。这就使得redis在......
  • Java客户端SpringDataRedis(RedisTemplate使用)
    文章目录⛄概述⛄快速入门❄️❄️导入依赖❄️❄️配置文件❄️❄️测试代码⛄数据化序列器⛄StringRedisTemplate⛄RedisTemplate的两种序列化实践方案总结⛄概述SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,......