首页 > 数据库 >Redis中集合的底层实现原理

Redis中集合的底层实现原理

时间:2024-06-22 21:57:38浏览次数:23  
标签:zipList 元素 Redis 列表 链表 集合 entry 节点 底层

        Redis 中对于Set类型的底层实现,直接采用了hashTable。但对于Hash、ZSet、List集 合的底层实现进行了特殊的设计,使其保证了Redis的高性能。

        对于Hash与ZSet集合,其底层的实现实际有两种:压缩列表zipList,与跳跃列表skipList。这两种实现对于用户来说是透明的,但用户写入不同的数据,系统会自动使用不同的实现。 只有同时满足配置文件redis.conf中相关集合元素数量阈值与元素大小阈值两个条件,使用的就是压缩列表zipList,只要有一个条件不满足使用的就是跳跃列表skipList。例如,对于 ZSet 集合中这两个条件如下:

  1. 集合元素个数小于redis.conf中zset-max-ziplist-entries 属性的值,其默认值为128
  2. 每个集合元素大小都小于redis.conf中 zset-max-ziplist-value属性的值,其默认值为64 字节

1、zipList

        zipList,通常称为压缩列表,是一个经过特殊编码的用于存储字符串或整数的双向链表。 其底层数据结构由三部分构成:head、entries与end。这三部分在内存上是连续存放的。

(1)head

        head 又由三部分构成:

  • zlbytes:占4个字节,用于存放zipList列表整体数据结构所占的字节数,包括zlbytes 本身的长度。
  • zltail:占4个字节,用于存放zipList中最后一个entry在整个数据结构中的偏移量(字节)。该数据的存在可以快速定位列表的尾entry位置,以方便操作。
  • zllen:占2字节,用于存放列表包含的entry个数。由于其只有16位,所以zipList最多 可以含有的entry个数为2^{16}-1 = 65535 个。

(2)entries

        entries 是真正的列表,由很多的列表元素entry构成。由于不同的元素类型、数值的不 同,从而导致每个entry的长度不同。 每个entry 由三部分构成:

  • prevlength:该部分用于记录上一个entry的长度,以实现逆序遍历。默认长度为1字节, 只要上一个entry的长度<254字节,prevlength就占1字节,否则其会自动扩展为3字 节长度。
  • encoding:该部分用于标志后面的data的具体类型。如果data为整数类型,encoding固定长度为1字节。如果data为字符串类型,则encoding长度可能会是1字节、2字 节或5字节。data字符串不同的长度,对应着不同的encoding长度。
  • data:真正存储的数据。数据类型只能是整数类型或字符串类型。不同的数据占用的字 节长度不同。

(3)end

        end 只包含一部分,称为zlend。占1个字节,值固定为255,即二进制位为全1,表示 一个zipList 列表的结束。

2、listPack

对于ziplist,实现复杂,为了逆序遍历,每个entry中包含前一个entry的长度,这样会导致在ziplist 中间修改或者插入entry时需要进行级联更新。在高并发的写操作场景下会极度降低Redis的性能。为了实现更紧凑、更快的解析,更简单的实现,重写实现了ziplist, 并命名为listPack。 在Redis 7.0 中,已经将zipList全部替换为了listPack,但为了兼容性,在配置中也保留 了zipList 的相关属性。

        listPack 也是一个经过特殊编码的用于存储字符串或整数的双向链表。其底层数据结构也由三部分构成:head、entries与end,且这三部分在内存上也是连续存放的。 listPack与zipList的重大区别在head与每个entry的结构上,表示列表结束的end与zipList 的zlend 是相同的,占一个字节,且8位全为1。

(1)head

        head 由两部分构成:

  • totalBytes:占4个字节,用于存放listPack列表整体数据结构所占的字节数,包括 totalBytes 本身的长度。
  • elemNum:占2字节,用于存放列表包含的entry个数。其意义与zipList中zllen的相同。

        与zipList 的 head 相比,没有了记录最后一个entry偏移量的zltail。

(2)entries

        entries 也是 listPack 中真正的列表,由很多的列表元素entry构成。由于不同的元素类型、数值的不同,从而导致每个entry的长度不同。但与zipList的entry结构相比,listPack 的entry 结构发生了较大变化。

        其中最大的变化就是没有了记录前一个entry长度的prevlength,而增加了记录当前 entry 长度的element-total-len。而这个改变仍然可以实现逆序遍历,但却避免了由于在列表中间修改或插入entry时引发的级联更新。

        每个entry 仍由三部分构成:

  • encoding:该部分用于标志后面的data的具体类型。如果data为整数类型,encoding 长度可能会是1、2、3、4、5或9字节。不同的字节长度,其标识位不同。如果data 为字符串类型,则encoding长度可能会是1、2或5字节。data字符串不同的长度,对应着不同的encoding长度。
  • data:真正存储的数据。数据类型只能是整数类型或字符串类型。不同的数据占用的字 节长度不同。
  • element-total-len:该部分用于记录当前entry的长度,用于实现逆序遍历。由于其特殊 的记录方式,使其本身占有的字节数据可能会是1、2、3、4或5字节。

3、skipList

        skipList,跳跃列表,简称跳表,是一种随机化的数据结构,基于并联的链表,实现简单, 查找效率较高。简单来说跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能。 也正是这个跳跃功能,使得在查找元素时,能够提供较高的效率。

(1)skipList 原理

        假设有一个带头尾结点的有序链表。

        在该链表中,如果要查找某个数据,需要从头开始逐个进行比较,直到找到包含数据的 那个节点,或者找到第一个比给定数据大的节点,或者找到最后尾结点,后两种都属于没有找到的情况。同样,当我们要插入新数据的时候,也要经历同样的查找过程,从而确定插入位置。为了提升查找效率,在偶数结点上增加一个指针,让其指向下一个偶数结点。

        这样所有偶数结点就连成了一个新的链表(简称高层链表),当然,高层链表包含的节点个数只是原来链表的一半。此时再想查找某个数据时,先沿着高层链表进行查找。当遇到 第一个比待查数据大的节点时,立即从该大节点的前一个节点回到原链表中进行查找。例如,若想插入一个数据20,则先在(8,19,31,42)的链表中查找,找到第一个比20大的节点31,然后再在高层链表中找到31节点的前一个节点19,然后再在原链表中获取到其下一 个节点值为23。比20大,则将20插入到19节点与23节点之间。若插入的是25,比节点 23 大,则插入到23节点与31节点之间。 该方式明显可以减少比较次数,提高查找效率。如果链表元素较多,为了进一步提升查找效率,可以将原链表构建为三层链表,或再高层级链表。

        层级越高,查找效率就会越高。

(2)存在的问题

        这种对链表分层级的方式从原理上看确实提升了查找效率,但在实际操作时就出现了问题:由于固定序号的元素拥有固定层级,所以列表元素出现增加或删除的情况下,会导致列表整体元素层级大调整,但这样势必会大大降低系统性能。 例如,对于划分两级的链表,可以规定奇数结点为高层级链表,偶数结点为低层级链表。 对于划分三级的链表,可以按照节点序号与3取模结果进行划分。但如果插入了新的节点, 或删除的原来的某些节点,那么定会按照原来的层级划分规则进行重新层级划分,那么势必会大大降低系统性能。

(3)优化

        为了避免前面的问题,skipList采用了随机分配层级方式。即在确定了总层级后,每添加一个新的元素时会自动为其随机分配一个层级。这种随机性就解决了节点序号与层级间的固定关系问题。

        上图演示了列表在生成过程中为每个元素随机分配层级的过程。从这个skiplist的创建和插入过程可以看出,每一个节点的层级数都是随机分配的,而且新插入一个节点不会影响到其它节点的层级数。只需要修改插入节点前后的指针,而不需对很多节点都进行调整。这就降低了插入操作的复杂度。 skipList 指的就是除了最下面第1层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针跳过了一些节点,并且越高层级的链表跳过的节点越多。在查找数据的时先在高层级链表中进行查找,然后逐层降低,最终可能会降到第1层链表来精确地确定数据位置。在这个过程中由于跳过了一些节点,从而加快了查找速度。

4、quickLIst

(1)什么是quickList

        quickList,快速列表,quickList 本身是一个双向无循环链表,它的每一个节点都是一个 zipList。从 Redis3.2版本开始,对于List的底层实现,使用quickList替代了zipList 和 linkedList。 zipList 与 linkedList 都存在有明显不足,而quickList 则对它们进行了改进:吸取了zipList 和 linkedList 的优点,避开了它们的不足。 quickList 本质上是 zipList 和 linkedList 的混合体。其将 linkedList 按段切分,每一段使用 zipList 来紧凑存储若干真正的数据元素,多个 zipList 之间使用双向指针串接起来。当然,对于每个zipList 中最多可存放多大容量的数据元素,在配置文件中通过list-max-ziplist-size 属性可以指定。

(2)检索操作

        为了更深入的理解quickList的工作原理,通过对检索、插入、删除等操作的实现分析来 加深理解。

        对于List 元素的检索,都是以其索引index为依据的。quickList由一个个的zipList构成,每个zipList 的 zllen 中记录的就是当前zipList中包含的entry的个数,即包含的真正数据元素的个数。根据要检索元素的index,从 quickList的头节点开始,逐个对zipList的zllen做sum 求和,直到找到第一个求和后sum大于index的zipList,那么要检索的这个元素就在这个 zipList 中。

(3)插入操作

        由于zipList 是有大小限制的,所以在quickList中插入一个元素在逻辑上相对就比较复杂 一些。假设要插入的元素的大小为insertBytes,而查找到的插入位置所在的zipList当前的大小为zlBytes,那么具体可分为下面几种情况:

情况一:当insertBytes + zlBytes <= list-max-ziplist-size 时,直接插入到 zipList 中相应位置即可

情况二:当insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的首部位置,此时需要查看该zipList的前一个zipList的大小prev_zlBytes。

  • 若insertBytes + prev_zlBytes<= list-max-ziplist-size 时,直接将元素插入到前一个 zipList 的尾部位置即可
  • 若insertBytes + prev_zlBytes> list-max-ziplist-size 时,直接将元素自己构建为一个新 的zipList,并连入quickList 中

情况三:当insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的尾部位置,此时需要查看该zipList的后一个zipList的大小next_zlBytes。

  • 若insertBytes + next_zlBytes<= list-max-ziplist-size 时,直接将元素插入到后一个zipList 的头部位置即可
  • 若insertBytes + next_zlBytes> list-max-ziplist-size 时,直接将元素自己构建为一个新的zipList,并连入quickList 中

情况四:当insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的中间位置,则将当前zipList分割为两个zipList连接入quickList中,然后将元素插入到分割后的前面zipList 的尾部位置

(4)删除操作

        对于删除操作,只需要注意一点,在相应的zipList中删除元素后,该zipList中是否还有 元素。如果没有其它元素了,则将该zipList删除,将其前后两个zipList相连接。

        前面讲述的Redis的各种特殊数据结构的设计,不仅极大提升了Redis的性能,并且还使得Redis可以支持的key的数量、集合value中可以支持的元素数量可以非常庞大。

  • Redis 最多可以处理2^{32}个key(约42亿),并且在实践中经过测试,每个Redis实例至少可以处理 2.5 亿个key。
  • 每个Hash、List、Set、ZSet集合都可以包含 2^{32}个元素。

标签:zipList,元素,Redis,列表,链表,集合,entry,节点,底层
From: https://blog.csdn.net/zhx3310228972/article/details/139887818

相关文章

  • Redis作为常见的缓存工具,我们是如何进行redis缓存持久化的呢?
    Redis的数据是全部存储在内存之中,但如果这时候Redis服务宕机,那存储在内存中的数据也会一并丢失,所以为了让redis的数据避免丢失或者是少丢失一点,就要利用策略来将redis的数据存入到磁盘之中,所以就诞生了redis的持久化。即Redis持久化的意义就是为了保证突然宕机,内存数据不会全部......
  • 集合通信库初步调研 NCCL、BCCL、TCCL、ACCL、HCCL
    幕后黑手是否基于NCCL扩展是否支持异构通信核心设计目标是否开源BCCL百度是是故障诊断容错性能优化否TCCL腾讯是是为腾讯星脉网络定制通信库极致优化性能否ACCL阿里不详不详面向阿里云灵骏架构设计优化性能否HCCL华为不详否基于昇腾硬件否不同厂商的集合通信库,就是针对他们网......
  • Map集合之HashMap细说
            最近在看面试题,看到了hashmap相关的知识,面试中问的也挺多的,然后我这里记录下来,供大家学习。Hashmap为什么线程不安全jdk1.7中,在扩容的时候因为使用头插法导致链表需要倒转,从而可能出现循环链表问题或者数据丢失的问题jdk1.8中,在put方法中,假设A,B两个线......
  • Java 面试题:如何保证集合是线程安全的? ConcurrentHashMap 如何实现高效地线程安全?
    在多线程编程中,保证集合的线程安全是一个常见而又重要的问题。线程安全意味着多个线程可以同时访问集合而不会导致数据不一致或程序崩溃。在Java中,确保集合线程安全的方法有多种,包括使用同步包装类、锁机制以及并发集合类。最简单的方法是使用Collections.synchronized......
  • 一条Redis命令是如何执行的?
    一条Redis命令是如何执行的?源码结构核心数据结构redisServerredisClientredisDbredisObjectaeEventLoop核心流程redis启动流程main()主循环aeEventProcess执行过程命令执行的流程过程1(redis启动)过程2(客户端与服务端建立链接)过程3(客户端发送命令给服务端)过程4(写就绪将......
  • redis持久化操作【随记】
    持久化Redis它是将数据保存到内存当中,内存里的数据最大特点:断电易失.保存在内存的数据就没有了.如果如果这些数据还有用,业务使用啥的,不能就让它这么没有了.redis当中提供持久化机制,说白了,将内存的数据—->写入到磁盘.–>持久化.1rdb方式redisdatabase,持......
  • 深入解析Redis:从基础到高可用性
    引言在现代应用程序中,数据的高性能、高可用性和一致性至关重要。Redis作为一种开源的内存数据结构存储,不仅提供了极快的读写速度,还支持多种数据结构和高可用性机制。本文将深入探讨Redis的基础知识、关键特性、常见应用场景以及其高可用性机制——主从复制和哨兵。Redis简介......
  • 分享一个go源码的均匀采样底层实现原理
    //int31n也就是下面这个函数,跟上面Int31n效果是一样的.但是效率更高.算法不一样.这个算法非常精彩,效率也更高.//int31nreturns,asanint32,anon-negativepseudo-randomnumberinthehalf-openinterval[0,n).//nmustbe>0,butint31ndoesnotcheckthis;......
  • 【初阶数据结构】深入解析带头双向循环链表:探索底层逻辑
    ......
  • java集合使用中的注意事项
    集合判断是否为空判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size()==0 的方式这是因为isEmpty()方法的可读性更好,并且时间复杂度为O(1)。绝大部分我们使用的集合的size()方法的时间复杂度也是O(1),不过,也有很多复杂度不是O(1)的,比如java.util.c......