想知道Redis底层数据结构如何实现的吗?先看这五种基础结构!

Redis底层数据结构

数据结构大致来说是这么实现的,由几种基本类型的,组成redis对应的各种数据结构。
在这里插入图片描述
在这里插入图片描述

所以下面我们先来了解一下这几种基本的数据结构。

动态字符串

SDS:

SDS是redis里面定义的一种存储字符串的数据结构。

传统的c语言字符串,获取字符串长度需要运算;字符串数组通常有结束标识,非二进制安全的;字符串一旦创建,就不能修改了(底层是char数组,当然不能修改了)。基于以上问题,redis创建了一种动态字符串结构SDS。

{'a','b','c','/0'}

由于redis是C语言实现的。所以SDS实际上是一个结构体,结构体里面包含内容有一个字符数组。有字符数组保存的字符串的字节数length,有字符数组申请的总字节数alloc,还有不同SDS头的类型 flags,决定SDS头的大小。

SDS分为头部分和数据部分。

数据部分就是字符数组。数据存放的地方。头部分就是剩下的三个信息,用于存放SDS相关的信息。

length:length对应的值是数据部分里面存储的数据的字节数。例如有一个SDS存储的是hello,length记录的值就位5,但实际上,数据部分的数组里面有六个空间,最后一位是\0.但是如果读这个字符串,只看length,length多少就读到几,不同于c语言找到\0读完。

alloc:字符数组申请的总字节数。也不包含结束标识。这个申请就是字节数组需要多少空间。例如起始值是hello,那就起始的alloc就是5.动态扩容的过程除外。

动态扩容:

动态扩容就是利用了在一个字符串上追加内容,比如hello拼world。首先会申请新的内存空间,这里有两种情况

  • 第一种就是拼加的长度小于1M,新空间就是追扩展后的长度 * 2 + 1。这个+1就是给\0 个位置。拼world后的alloc就是20,数据部分的大小是21.
  • 第二种就是如果拼接部分的大小超过了1M。那么新空间为 扩展后字符串的长度 + 1M + 1.这种行为称为内存预分配。

使用SDS字符串的优点就是:

  • 获取字符串长度的时间复杂度是O(1),直接就能拿到
  • 支持动态扩容
  • 支持内存预分配,提升性能
  • 二进制安全,遇见/0这种标识符不会截止,会继续读下去。

intSet

intSet是Redis中set集合的一种实现方式,基于整数数组实现。

结构:

intSet中是根据encoding统一编码的,统一编码的原因就是因为数组是一片连续的内存空间,c语言通过指针寻址,数组中元素相同的字节数按数组下角标寻址。

length:数组中元素个数

contents[]:存储intset中的元素,数组中元素的编码方式实际上依赖于encoding,并不依赖于定义数组时的编码。content只是一个指向数组的指针,对于数组中元素的操作,并不依赖于c语言本身提供的函数,实际上还是redis自身做的。也就是说,如果此时encoding是 2 字节。那么他就在content里面占两个连续的空间。

intset结构在这里插入图片描述

encoding包含三种模式,表示存储的整数大小不同

在这里插入图片描述

分别对应2,4,8字节整数。

为了方便查找,Redis会将intset所有的整数按照升序依次保存在contents里。
在这里插入图片描述

类型升级机制:如上图,此时编码方式是int,如果此时插入了一个long类型的数据,那么此时的encoding已经不能满足新插入的数据了,那么就要进行类型升级。把encoding升级到能够满足新插入数据的模式。

  • 首先升级编码,按照新的编码方式扩容数组以及元素个数。
  • 倒序,将原来的元素拷贝到扩容后的正确的角标位置上
  • 将待添加的元素放入数组末尾。
  • 修改intset的encoding属性。修改length属性,加新插入元素个数个。

为什么倒序插入:倒序插入能够保证后一个元素的内存空间不被前一个元素占用。如果是正序,第一个元素升级之后,占用的空间必然会变大,此时第二个元素的还在原来的角标出的空间,第二个元素的空间就会被占用,而倒序,从后面空的空间开始拷贝,这样元素和元素之间不会发生冲突。 .

源码:

在这里插入图片描述
在这里插入图片描述

intSet归根结底,是一个set结构,redis内部帮我们实现了set的不可重复性。

Redis内部保证了intSet中元素的有序性。

具备类型升级机制。能够节约内存空间。

底层采用二分查找,通过二分查找寻找元素插入的位置,按照顺序找到元素应该在的位置,保证了有序性。

在这里插入图片描述

intset最好在数据量不多的时候使用,因为数组是一片连续的内存空间,数据量较大的话一次性申请一片较大的连续内存空间也很不方便。

Dict

Redis是一个 key - value型的数据库。这种映射关系是通过dict实现的。Dict由三部分组成,哈希表,哈希节点,字典。

在这里插入图片描述

当我们向dict添加entry的时候,redis给key计算出一个hash值。通过这个哈希值和哈希表的大小的掩码做与运算。找出存放在数组里面的索引位置。如果发生了哈希冲突,那么采用头插法,将新的节点插进数组索引位置对应的链表上。根据dictentry的结构分析,它是由next指针的。如果插在尾部就意味着要next到最后一个节点去插入。头插法直接让新节点next成为旧的头结点。很快。

在这里插入图片描述

redis给dict定义的结构体dict。注意到有个参数叫ht[2],内置两个hash表的数组对象。一个是放的是当前数据,另一个一般是空的,用来做rehash的。后面的两个参数就是和rehash有关的参数。

下面详细介绍一下rehash。

dict的哈希表是数组加单向链表实现的。集合中元素多的时候,哈希冲突会多,链表长度就会变长。

dict在每次新增entry的时候都会检查loadFactor(loadFactor = userd/size)

  • LoadFactor >= 1 的时候,并且redis没有在bgsave和bgrewrite的时候,会扩容哈希表

    后面那哥俩会占用大量的系统资源。所以在他俩进行的时候就先不扩容了。避免资源紧张

  • loadFactor > 5 的时候会扩容。在这里插入图片描述

注意:size的大小总是2的n次方,比如说当前容量是8,userd是9了,你就得扩容到16

对hash表的元素进行添加或删除的时候,都会检查 负载因子,到达一定阈值就会扩容或者收缩。不管是扩容还是收缩,由于哈希表是数组加链表。数组是不可变得。所以一定会创建一个新的哈希表。哈希表的size就会发生变化。key的查询和size的掩码有关。这样就需要对每一个key进行索引的重新计算。插入新的哈希表。这个过程就叫rehash。

rehash过程

  1. 看当前的操作是扩容还是收缩
  2. 如果是扩容,新的size就是 新的userd + 1 的第一个2 的n次方。如果是收缩,那么就是userd后的第一个2的n次方。
  3. 按照新的 size申请新的内存空间,创建哈希表,赋值给dict里的dicth[1]。
  4. 将rehashid设置成1、同时将dicth[0]里面的 entry rehash 进dicth[1]里面。(entry过去之后,原dicth里面就没有了)。
  5. 将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存
  6. rehashid赋值成1
  7. rehash的过程中。新增的操作就直接写入h1就行了。改,查,删这样的操作就在两个哈希表里面找。在哪个里面就用那个表。这样能保证h[0]里面的数据只减不增。

redis采用渐进式rehash,在每次访问dict的时候进行一次rehash。这样能够 避免数据量过大的时候做rehash造成堵塞。

缺点:dict这种结构大量使用指针,会产生很多的内存碎片。指针还要占据字节,浪费空间

ZipList

zipList是一种特殊的压缩列表,他是双端的。

属性类型长度用途
zlbytesuint32_t4 字节记录整个压缩列表占用的内存字节数
zltailuint32_t4 字节记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,可以确定表尾节点的地址。
zllenuint16_t2 字节记录了压缩列表包含的节点数量。 最大值为UINT16_MAX (65534),如果超过这个值,此处会记录为65535,但节点的真实数量需要遍历整个压缩列表才能计算得出。
entry列表节点不定压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
zlenduint8_t1 字节特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。
![]
](https://img-blog.csdnimg.cn/855912c964664cf993c255d8439706c1.png)

ZipListEntry

ZipList 中的Entry并不像普通链表那样记录前后节点的指针,因为记录两个指针要占用16个字节,浪费内存。而是采用了下面的结构:

在这里插入图片描述

  • previous_entry_length:前一节点的长度,占1个或5个字节。

    • 如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
    • 如果前一节点的长度大于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据
  • encoding:编码属性,记录content的数据类型(字符串还是整数)以及长度,占用1个、2个或5个字节

  • contents:负责保存节点的数据,可以是字符串或整数

ZipList中所有存储长度的数值均采用小端字节序,即低位字节在前,高位字节在后。例如:数值0x1234,采用小端字节序后实际存储值为:0x3412

编码:

11开头是整数类型编码

10 ,00 01是字符串类型。content是字符串型 0 -12 直接存储在encoding后四位。不用放content里面

ziplist的连锁更新问题:

根据ziplist的 entry结构。现在做一个假设,有n个连续长度为253字节的entry,他的previous_entry_length 保存上一个节点的大小。用一个字节记录。现在加了一个新的节点,这个节点是255个字节,新加入的这个节点的后面的节点 的previousentrylength就会变成 5 字节,这样这一个节点的大小就会产生变化,后面的也会连锁更新。这种行为叫做连锁更新问题。

解决方法最好一个节点少存点。紧凑列表也能解决,这里不做具体介绍了。推荐每个节点少存点数据,redis内存数据库,重视性能的。

总结:

  • 压缩列表的可以看做一种连续内存空间的"双向链表"
  • 列表的节点之间不是通过指针连接,而是记录上一节点和本节点长度来寻址,内存占用较低
  • 如果列表数据过多,导致链表过长,可能影响查询性能
  • 增或删较大数据时有可能发生连续更新问题

QuickList

zipList虽然节省内存,但是在内存里面必须是连续空间。申请内存有可能会出现效率低的情况。

应运而生了一种数据结构交quickList,这哥们是一个双向链表,节点是zipList。在这里插入图片描述

为了避免QuickList中的每个ZipList中entry过多,Redis提供了一个配置项:list-max-ziplist-size来限制。
如果值为正,则代表ZipList的允许的entry个数的最大值
如果值为负,则代表ZipList的最大内存大小,分5种情况:

  • -1:每个ZipList的内存占用不能超过4kb
  • -2:每个ZipList的内存占用不能超过8kb
  • -3:每个ZipList的内存占用不能超过16kb
  • -4:每个ZipList的内存占用不能超过32kb
  • -5:每个ZipList的内存占用不能超过64kb

其默认值为 -2:

QuickList的特点:

  • 是一个节点为ZipList的双端链表
  • 节点采用ZipList,解决了传统链表的内存占用问题
  • 控制了ZipList大小,解决连续内存空间申请效率问题
  • 中间节点可以压缩,进一步节省了内存

SkipList(传说中的跳表)

这哥们首先是一个链表,他的元素按升序排列,节点包含多个指针,跨度不同。在这里插入图片描述

对于一个单向链表来说,及时链表中存储的元素是有序的,找到具体的元素也需要沿着头结点往下找。效率很低。所以skipList每个节点都包含多层指针,像数据库里面的索引一样。查找的时候找到当层索引,去下一级再找对应的索引。 每一层都找到关键的节点。

在这里插入图片描述

在这里插入图片描述

跳表时间复杂度:分析一下 ,第一层从中间开始分开,第一层节点个数大约是 n / 2 ,第二级索引的结点个数大约就是n/4,第三级索引的结点个数大约就是n/8,依次类推…

也就是说,第k级索引的结点个数是第k-1级索引的结点个数的1/2,那第k级索引结点的个数就是n/(2^k)

跳表的高度可以算出是 log2 n - 1.时间复杂度O(logn).

总结:

  • skipList是一个双向链表,每个节点包含score 和 ele。
  • 节点按照score 排序。 如果节点的值一样则按照ele字典排序
  • 每个节点都能包含多层指针,层数是1 到 32 之间的随机数
  • 不同层数指针到下一节点的跨度不同。
  • 增删改查效率和红黑树一样
    引的结点个数是第k-1级索引的结点个数的1/2,那第k级索引结点的个数就是n/(2^k)

跳表的高度可以算出是 log2 n - 1.时间复杂度O(logn).

总结:

  • skipList是一个双向链表,每个节点包含score 和 ele。
  • 节点按照score 排序。 如果节点的值一样则按照ele字典排序
  • 每个节点都能包含多层指针,层数是1 到 32 之间的随机数
  • 不同层数指针到下一节点的跨度不同。
  • 增删改查效率和红黑树一样
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值