Redis数据结构之链表与字典的使用

今天我们来聊一聊redis中的链表与字典,具体如下:

链表

关于链表的基础概念其实你在学习redis之前一定积累了不少,所以本文将默认你已经掌握了链表相关的基础知识,而redis的链表其实也就是普通的链表~

因为redis是使用c语言编写的,因此redis的数据结构的定义都是使用c语法定义的,你不需要完全理解下方c语言声明结构体的语法,但我认为依靠大家的java知识也能理解这就像是在java中定义了一个链表对象

redis链表节点的结构

很明显,当每一个节点内记录了前后两个节点位置之后,链表节点之间就能够彼此前后相连,组成双向通行车道(可以双向遍历)

redis链表的表示

上面讲解了redis的链表的节点表示,并由此引申了一下可以借此构建redis双端链表,而事实上,对于每一个存在的双端链表,redis使用一个list结构来表示

很明显,你看到三个好像是返回值为void的函数,但是看不懂c语法,没关系,传统后端功夫,自然是点到为止

redis链表用在哪

我不想现在就告诉你,链表被广泛用于实现redis的各种功能,比如列表键、发布于订阅、慢查询、监视器等,等我们后面讲到这几部分的时候,白泽再结合链表和你细说~

字典

和链表一样,redis所使用的c语言并没有内置字典这种数据结构,因此redis构建了自己的字典实现。如果你学过数据结构,你会发现redis的字典事实上就是数据结构中的邻接表,即使没学过,往下看就好啦~

redis字典结构总览

数组 + 链表 ==> 邻接表,实锤

redis字典结构分解

还记得吗,上面我们说redis链表可以用list描述,但是链表存储的数据本质上,是由一系列listnode节点通过前后指针相连存储的;类似的,redis字典可以用如下dict描述,但是字典存储的数据本质上,是由数组 + 若干链表组合得到的数据结构存储的,字典dict结构如下:

现在你只需要关注其中的哈希表数组ht[2],它的数据类型为dictht,因此也是一种复合的数据结构,如下:

哈希表dictht是redis字典的核心,dictht的四个属性中,size、sizemax、used都是用于描述table属性整体状态。看到这你就明白了,dictht的核心是dictentry类型的table属性(再次提醒,如果没有c语言的基础,本文中一切你看不懂的语法,包括数据类型,你只需要一眼带过即可,我们的目的是学习redis的设计思想)

table属性是一个数组,数组中的每个元素都是一个指向dictentry结构的指针,每个dictentry结构保存一个键值对,并含有一个指向下一个dictentry的指针,结构如下:

哈希算法

我们知道,字典是用来存储数据的,并且是以键值对的形式存储的,那么我每次存入一个键值对放在字典的哪里?这就是哈希算法为你解决的事情:程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面

比如我已经有下面这个字典,然后要插入一个键值对数据:k1 : v1,则程序有如下计算过程:(用户只是往redis服务器中插入了一条数据,下面都是程序内部的工作~)

解决键冲突

键冲突:当不同的key值计算得到的dictentry索引值相同时,就称发生键冲突(我要插入的位置已经被占用了,插入使得链表长度由1变多,当然第一次插入不算冲突)

解决方法:

就像上面我要插入一个k1 :v1的键值对,并计算得到插入位置的索引为1(但是distentry数组中索引为1的位置已经有k0 :v0键值对存放了),因此程序会在哈希表ht[0]的dictentry数组的索引为1的位置上插入一个dictentry节点,放在原本链表首部的前一位置(抢占首位),其中存放着k1 : v1键值对,插入后的图如下:

你可能疑惑新插入的键值对的位置在每个dictentry链表的最前面,而不是尾部,原因是每个dictentry中除了保存键值对之外,只记录了下一个dictentry的地址(上面我已经给出了dictentry的结构了~),程序无法直接得到dictentry链表的最后一个节点,但可以直接得到第一个节点(通过dictentry数组索引直接定位),因此每次插入的dictentry节点(键值对)都将直接插入到对应索引的链表的头部(因此dictentry数组的内容是不断在变的)

一句话来说:distentry数组帮助使用索引定位,distentry链表,用于处理冲突,不断维护所存储的键值对数据

rehash

随着操作的不断执行(增、删、改、查),哈希表保存的键值对会逐渐增多或者减少,为了让哈希表的负载因子维持在一个合理范围内,当哈希表保存的键值对数量太多或太少时,程序会对哈希表的大小进行相应的扩展或者收缩(不知道你是否记得还有一个哈希表ht[1]的存在,这个表就是为了和ht[0]配合进行rehash而存在的)

rehash步骤:

为字典的ht[1]哈希表分配空间

如果程序执行扩展操作:

ht[1].size = 第一个大于等于ht[0].used * 2(ht[0]已经使用的空间大小乘2)的2的n次方幂

如果程序执行收缩操作:

ht[1].size = 第一个大于等于ht[0].used(ht[0]已经使用的空间大小)的2的n次方幂

将保存在ht[0]上的键值对rehash到ht[1]上,因为size不同,所以是重新hash,而不是整体复制

当ht[0]内键值对全部迁移到ht[1]中后,释放ht[0],然后将ht[1]和ht[0]的互换(rehash结束),此时ht[0]就是一个rehash后的哈希表,而ht[1]依旧为空表,为下次rehash做准备

渐进式rehash

上面提到的在哈希表ht[0]的负载因子过大或者过小会触发rehash,但是,事实上rehash迁移的过程不是一蹴而就的(很明显,如果数据ht[0]的数据很多,每次rehash如果都迁移全部数据,需要花费较大时间等待,用户在rehash期间访问redis服务器将会陷入无响应的状态)

渐进式过程:

将rehash的过程分摊在后续的每次增、删、改、查操作上,在rehash期间,每次对字典执行操作,程序除了执行指定操作外,还会顺带将ht[0]哈希表在rehashidx索引(从0开始,-1表示rehash未开始)上的所有键值对rehash到ht[1],当每次局部rehash工作完成后,程序将rehashidx属性的值增一

注意:每次对字典进行增、删、改、查会在ht[0]和ht[1]上同时进行,比如查找一个键,则会现在ht[0]上查找,没找到再去ht[1]上查找,诸如此类,除了增加操作每次都将直接hash到ht[1]上,不会对ht[0]执行任何添加操作

到此这篇关于redis数据结构之链表与字典的使用的文章就介绍到这了,更多相关redis 链表与字典内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!

(0)
上一篇 2022年3月21日
下一篇 2022年3月21日

相关推荐