「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」前端
前言
应用场景
- 在Redis中有不少场景都是用了字典做为底层数据结构!咱们使用最多的应该是redis的库的设置和五种基本数据类型的Hash结构数据!
- 在上一篇【redis前传】中咱们学习了list数据结构。今天咱们继续学习主流数据结构Hash。
- 在redis内部有字典结构、hash结构可是这里的hash和咱们平时熟知的redis基础数据的hash并非一个意思!咱们简单的将字典结构、hash结构理解成redis更加底层的一种抽象结构。平时咱们使用的hash基础数据结构理解成hash工具

- 而今天咱们的主角就是五种数据结构的Hash分析。他的底层使用了字典这个结构。字典结构内部使用的是底层的hash结构。有点绕!好好理解你行的
哈希表

- 上面这张图诠释了做为redis底层结构的Hash。在内部redis称之为dictht 。 后面咱们为何和以前的hash结构冲突咱们都已类名为准叫作dictht类。
- 在hictht类中有四个属性分别是table 、 size 、 sizemask 、 used ; 其中table就是一个数组;数组中元素是另一个类叫作dictEntry类。
- dictEntry就是真正存储数据的。内部是key、value存储结构。一个简单的哈希表就如图所示。数据最终会存储在table中的dictEntry对象中。
- 至于为何sizemask = size -1 ; 这个是为了在计算hash索引时须要用到的。那为何不直接使用size-1而是经过一个变量来承接呢?这个吧!!!我也不知道。容我去百度百度。
数组节点
- 上面的哈希表是否是很熟悉,这不和咱们Java中的Map数据结构一模一样吗。能够说是也能够说不是,二者很类似但也有区别的。
- 在上面中咱们提到数据最终是存储在哈希表里table数组里的元素。该元素叫dictEntry 。 下面咱们看看dictEntry结构如何吧!

- 经过左侧对dictEntry的定义咱们能够看出。dictEntry存储的值能够是指针、正数、浮点数各类数据类型!相似于Java中的Object属性。 对于上述的key没有啥真意的就是一个键。
- 既然是数组那么索引就是固定长度的,那么在有限的长度中确定会出现经典问题就是【hash冲突】。在Java中咱们是经过链表和红黑树来解决冲突的问题!在redis中是经过链表解决的。在dictEntry中经过next指针将冲突元素链接。
- 这里咱们就能够和Java中的Map结构进行理解。他们内部非常类似!!!
- 这里须要注意下在hash冲突时redis的确是经过链表进行存储的,可是因为哈希表(dictht)中没有记录每一个索引未中链表的尾部节点只有头结点信息因此。并且咱们也知道链表在查询上效率不佳,因此当发生哈希冲突时redis是将新加入的节点加入在链表的头部!

字典
多态字典
- 字典是本文开头提出的结构!也是redis中大量使用的一种底层数据结构。在redis中名叫作dict类。

- 经过图示咱们明确的看出内部是包含哈希表的。其实从名字上咱们也能够看出哈希表为何叫dictht 。 笔者这里认为是dicthashcodetable 。 意思就是字典表内部的一个hash相关的数组(仅我的理解)
- 以前也提到过redis内部不少地方会使用到字典!就比如咱们上学是用到【新华字典】、【成语词典】、【歇后语词典】等等。虽然名字叫法不同可是内部结构都是一部字典供咱们快速定位。而redis中dict内部就是经过type字段进行区分每一个字典的。而privdata是每部字典须要的特定参数。经过type和privdata就能够轻松实现各类功能不一样的字典,他有个专有名词叫~~多态字典~~
rehash
- 除了type 、 privdata之外剩下的就是ht 、 rehashidx了。其中ht是一个长度为2的数组。数组里元素就是咱们以前提到了哈希表(dictht) 。 ht为何长度为2 这就须要咱们了解下redis的rehash过程了。而rehashidx就是记录rehash的进度!在没有发生rehash的时候rehashidx=-1;
- 在实际使用过程当中在字典中咱们全部的数据都会存储在ht[0]对应的哈希表中。ht[1]永远都是一个空数组。这些都是为何rehash作准备,在正式开始以前咱们先来了解下redis为何须要rehash这个动做
- 首先咱们在哈希表中是一个定长数组发生冲突时内部是经过链表解决的。理论上一个哈希表能够存储足够的数据,这里的足够就是指空间容许的范围有多少存多少。可是咱们知道链表的特色就是新增、删除很快可是查询很慢,尤为是当链表很长的时候就会出现查询效率低下的问题!为了不链表过长redis就会在必定条件下对哈希表中数组长度的扩展从而解决局部链表过长的问题!
- 每次数组发生长度变化时,那么以前的hash值就须要从新经历一遍hash而后寻址index的过程。这个过程就叫作rehash 。

- 关于rehash和Java中Map的resize是同样的功能!Java中resize是直接new 出一片内存进行复制的并且他是每次进行2倍扩展。而redis的rehash稍微不一样基本上咱们也能够理解成2倍扩展!关于两块内存复制有点相似于JVM中垃圾回收有点相似。有时间咱们能够一块儿研究下JVM章节。
- 那么啥时候须要进行rehash呢?这里和Java的负载因子同样;可是除了负载因子这个空间考核之外redis还考虑一个性能的问题。由于在单线程的前提下咱们还要考虑客户端使用的感知性!单线程的意思就是执行命令是顺序执行的。总不能在咱们rehash的过程当中所有阻塞客户端的使用这对于操做体验上稳定性来讲是不友好的。

- 涉及到上述两个命令的咱们称之为后台命令结合负载因子产生以下条件



渐进式rehash
-
一直强调redis是单线程。那么什么叫单线程模型?就是对于redis服务来讲执行命令是线性操做!可是每一个客户端的命令是无序的,先到的就先进入队列redis服务从队列一次取出命令进行执行。除了客户端的命令还有一些系统生成的命令好比说咱们上面提到的rehash操做!数组
-
①、首先为了不阻塞客户端或者说尽可能控制阻塞的时间在客户端感知范围内,redis内部的rehash并非一次性操做而是一个按部就班的过程。一次仅复制一部分markdown
-
②、还记得以前咱们提到dict中rehashidx这个属性吗,他是记录rehash的进度。由于哈希表内部是一个数组而rehashidx就是记录这个数组的索引。从而咱们也能够知道每次rehash复制的时候是已一个索引完整链表为单元进行复制的。数据结构
-
③、除了新增之外的其余操做都会同时影响到ht[0]、ht[1] 由于在rehash过程当中两个数组都是在使用状态的工具
-
④、新增值的时候就只须要新增到ht[1]中。由于最终的目的就是将全部值同步到ht[1]中。而ht[0]的值会慢慢的变少;不必新增到ht[0]post
-
⑤、在rehash过程当中查找元素时会查找两个数组中的并集元素。这也就也是了为何再rehash过程新增元素只须要新增到ht[1]的缘由性能
总结
①、字典表在redis被普遍使用,基于字典表优秀的设计解决redis单线程问题学习
②、字典里包含哈希表,哈希表内部使用节点负责存储key、value
③、字典type实现多态字典用于多场景!
④、渐进式rehash解决服务卡顿问题