【Redis底层数据结构】

【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】

**开源地址:https://docs.qq.com/doc/DSmxTbFJ1cmN1R2dB **

双向链表:单端链表只能从链表头开始正向遍历,双向链表可以逆向遍历,每个节点需要保存前一个节点和后一个节点的引用

有序链表:插入元素时,将插入的元素与头结点及其后面的结点比较,找到合适的位置插入。

有迭代器的链表:单链表的基本操作中,大部分要用到依次遍历单链表中的每一个元素。当你新增一个对单链表的操作并需要使用遍历时,你就得重新写一个for循环而实现遍历。所以将迭代(遍历)作为一种基本的ADT(抽象数据类型)操作。链表中用于处理遍历、访问和更新的方法封装到一个新的迭代器类中。

跳跃表

跳跃表:跳跃表基于有序链表的扩展,在链表上建索引,每两个结点提取一个结点到上一级,我们把抽出来的那一级叫作索引,每个跳跃表节点的层高都是1至32之间的随机数。

举例说明:

比如给一个长度为7的有序链表,节点值依次是1->3->4->5。取出所有值为奇数的节点作为关键节点(索引),这个时候要插入一个值是2的新节点,就不需要将节点一个个比较,只要比较1,3,5,确定了值在1和3之间,就可以快速插入。

加一层索引之后,查找一个结点需要遍历的结点个数减少了,虽然增加了50%的额外空间,但是查找效率提高了,同理再加一级索引,这种链表加多级索引的结构,就是跳跃表。

索引是占内存的,原始链表中存储的可能是大的对象,索引结点只要存储关键值和几个指针,并不需要存储对象,当节点本身比较大或者元素数量比较多的时候,优势必然会被放大,而缺点则可以忽略。

问题:当大量的新节点通过逐层比较,最终插入到原链表之后,上层的索引节点会慢慢的不够用,那么这个时候要怎么选取一部分节点提到上一层呢?

抛硬币法:随机决定新节点是否选拔,每向上提拔一层的几率是50%。

原因:跳跃表的删除和添加节点是无法预测的,不能保证索引绝对分步均匀,不过可以让大体趋于均匀。

插入节点的工作流程:跳跃表插入操作的时间复杂度是O(logN),空间复杂度是 O(N)。

  • 第一步:新节点和上层索引节点逐个比较,找到原链表的插入位置,时间复杂度为O(logN)

  • 第二步:把索引插入到原链表,时间复杂度为O(1)

  • 第三步:随机决定新节点是否提升为上一级索引,结果为"正面"则提升,继续抛硬币,结果为"反面"则停止,时间复杂度为O(logN)

删除节点的工作流程:跳跃表删除操作的时间复杂度是O(logN)

  • 第一步:自上而下,查找第一次出现节点的索引,并逐层找到每一层对应的节点。时间复杂度为O(logN)

  • 第二步:删除每一层查找到的节点,如果该层只剩下1个节点,删除整个一层(原链表除外)。时间复杂度为O(logN)

跳跃表由zskiplistNode和skiplist两个结构组成,zskiplistNode用于表示跳跃表节点,zskiplist用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等。

字典

字典,顾名思义,通过字典(牛津字典等)前面的目录快速定位到所要查找的单词。

在C 语言中没有这种数据结构,所以这种数据结构是Redis自己创造的,字典中的键都是唯一的,通过键可以对值来进行查询或更改。

底层是通过哈希表实现的,而哈希表又基于数组,类似于key-value的结构形式进行存储的,它的值通过哈希函数映射为数组的下标。

那什么是哈希函数呢?不急,我们慢慢道来。

前面我们讲了通过数组的方式存储值,那么数组的值和数组的下标怎么建立关联关系呢?或者说,我们怎么通过数组的下标找到数组的值呢?

在学习 ASCII 编码的时候,我们知道,a可以用97这个数值表示,b可以用98这个数值表示,以此类推,我们就可以通过单个字母用数字表达。

有了字母,那么一个单词由多个字母组成,它又该如何表达呢?

假设我有一本字典,它有10000个单词,我其中一个单词就是ab,使用ASCII编码进行表达。

ab = 97 + 98 = 195

那么存储在数组中的下标为195,这就是字母表达的基本原理,但是如果只是这样还是远远不够的,因为会出现一个数组存储多个单词的情况。

举例说明:假设有个单词有 10 个字母,那么字典的某个单词为 zzzzzzzzzz ,转换为数字:zzzzzzzzzz = 26*10 = 260。

补充说明:这个时候会发现我一本字典里10000个单词,在260这个范围内肯定是不够存储10000个单词的,10000/260=39(38.4补一位),一个数组项它要存储39个单词。

解决方案:为了保证数值的唯一,让每个数组都能够只存储一个单词,进行升级, 将单词表示的数拆开,27 的幂乘以这些位数,有26个可能的字符,以及空格,一共27个。

ab = 97乘以27的一次幂加上98乘以27的零次幂 = 27*97 + 98 = 2717。解决了数组存储多个单词的问题,又引出新的问题数组分配大空间太多了。

举例说明:假设有个单词有 10 个字母,那么字典的某个单词为 zzzzzzzzzz ,转换为数字:zzzzzzzzzz = 26的9次幂 = 7000000000000

补充说明:数组中只有小部分存放了单词,其他空间都是空着的

解决方案:将巨大的整数范围压缩到可接受的数组范围内,可以通过取余解决,一个整数被另一个整数除后的余数。

举例说明:假设要把从0-99的数字(用large表示),压缩为从0-9的数字(用number表示),后者有10个数,所以变量range 的值为10,这个转换的表达式为:

补充说明:number = large % range。当一个整数被 10 整除时,余数是在0-9之间,把从0-99的数压缩为从0-9的数,压缩率为 10 :1。

使用哈希函数向数组插入数据后,这个数组就是哈希表,它的值就是通过上面这种方式映射到数组的下标上的。

这也就是哈希函数的工作模式,它把一个大范围的数字哈希转化成一个小范围的数字,这个小范围的数对应着数组的下标。

但是这种工作模式会有一点问题:把大的数字范围压缩到小的数字范围,会有几个不同的单词哈希化到同一个数组下标,这就是所谓的哈希冲突。

问题:那么如何解决哈希冲突呢?

开放地址法:指定的数组范围大小是存储数据的两倍,有一半的空间是空的。

当冲突产生时,通过(线性探测、二次探测以及再哈希法)方法找到数组的一个空位,把单词填入,不用哈希函数得到数组的下标。

线性探测中,如果哈希函数计算的原始下标是x, 线性探测就是x+1, x+2, x+3, 以此类推,而在二次探测中,探测的过程是x+1,

x+4, x+9, x+16。这二种方式都会有聚集情况。

什么是聚集呢?当哈希表快要满的时候,每插入新的数据,都要频繁的探测插入位置,很多位置都被前面插入的数据所占用了,这称为聚集。

再哈希法:依赖关键字的探测序列,把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长,步长在整个探测中是不变的,不过不同的关键字使用不同的步长。

链地址法:数组的每个数据项都创建一个子链表或子数组,那么数组内不直接存放单词,当产生冲突时,新的数据项直接存放到这个数组下标表示的链表中。

整数集合:顾名思义,用来保存整数值类型的集合,保证元素不会重复。

定义:

typedef struct intset{

//编码方式

uint32_t encoding;

//集合包含的元素数量

uint32_t length;

//保存元素的数组

int8_t contents[];

}intset;

contents数组声明为int8_t类型,但是contents数组并不保存任何int8_t类型的值,真正类型由encoding决定。比如:

  • encoding属性的值为INTSET_ENC_INT16,contents是int16_6类型的数组,数组里的每个项是int16_t类型的是整数值。

  • encoding属性的值为INTSET_ENC_INT32,contents是int32_t类型的数组,数组里的每个项是int32_t类型的整数值。

  • encoding属性的值为INTSET_ENC_INT64,contents是int64_t类型的数组,数组里的每个项是int64_t的整数值。

新增的元素类型比原集合元素类型的长度大的时候,根据新元素类型增加整数集合底层数组的容量,给新元素分配空间,

将底层数组现有的所有元素都转成与新元素相同类型的元素,把转换后的元素放到正确的位置,整个元素顺序是有序的,能极大地节省内存。

压缩列表

压缩列表,它是特殊编码的连续内存块组成的顺序型数据结构,压缩列表有任意多个节点(entry),每个节点有一个字节数组或者一个整数值。

压缩列表不是用某种算法对数据进行压缩,它将数据按照一定规则编码,放在一块连续的内存区域,目的是节省内存。

压缩列表包含以下:

zlbytes:记录整个压缩列表占用的内存字节数。

zltail:记录压缩列表表尾节点距离压缩列表的初始地址有多少字节。

zllen:记录压缩列表包含的节点数量。

zlend:用来标记压缩列表的末端。

entryX:列表的节点,包含

  • previous_entry_ength:记录压缩列表前一个字节的长度。

  • encoding:节点的encoding保存的是节点的content的内容类型以及长度。

  • content:content区域用于保存节点的内容,节点内容类型和长度由encoding决定。

总结:

简单字符串:SDS作为redis专门为字符串存取开发的数据结构,有获取字符串长度快,杜绝了缓存区的溢出,减少了修改字符串长度时所需的内存重分配次数,二进制安全,兼容部分C函数

链表:用作列表键、发布与订阅、慢查询、监视器等功能实现。

字典:用哈希表实现,字典有两个哈希表,一个正常使用,另一个用于rehash时使用,链地址法解决哈希冲突。

跳跃表:表中的节点按照分值大小进行排序。

整数集合:底层由数组构成,升级特性能尽可能的节省内存。

压缩列表:顺序型数据结构。


总结

以上就是今天要讲的内容,还希望各位读者大大能够在评论区积极参与讨论,给文章提出一些宝贵的意见或者建议📝,合理的内容,我会采纳更新博文,重新分享给大家。

🙏四连 关注🔎点赞👍收藏⭐️留言📝

感谢大家的支持,用心写博文分享给大家,你的支持(🔎点赞👍收藏⭐️留言📝)是对我创作的最大帮助。

🍊微信公众号:南北踏尘

🍊主页地址:java_wxid

🍊社区地址:幕后大佬

给读者大大的话

我本身是一个很普通的程序员,放在人堆里,除了与生俱来的🌹盛世美颜🌹、所剩不多的发量,就剩下180的大高个了。就是我这样的一个人,默默坚持写博文也有好多年了,有句老话说的好,🌕牛逼之前都是傻逼式的坚持🌕。希望自己可以通过大量的作品,时间的积累,个人魅力、运气和时机,可以打造属于自己的🌟技术影响力🌟。同时也希望自己可以成为一个🎄懂技术🎄,🎄懂业务🎄,🎄懂管理🎄的综合型人才,作为项目架构路线的总设计师,掌控全局的🌕团队大脑🌕,技术团队中的🍊绝对核心🍊是我未来几年不断前进的目标。


提示:以下都是资源分享,求个一键三连。

面试资料

福利大放送,🎉欢迎关注🔎点赞👍收藏⭐️留言📝,拜托了🙏,这对我真的很重要。

点击:面试资料

提取码:2021

200套PPT模板

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值