redis数据结构 Dict

Dict

redis中的值都是键值对(key-value)去保存数据的数据库,我们可以根据兼快速的实现增删改查。而键值对正是通过Dict实现的。
Dict中由三部分组成:
哈希表(DictHashTable),哈希节点(DictEntry),字典(Dict)

1. dictHt和dictEntry

在这里插入图片描述

  • table:entry数组,类似java中HashMap中的Entry
  • size:哈希表的大小,一般位2的n次方
  • sizemask: 哈希表大小掩码,一般是size - 1
  • used:元素的个数

在这里插入图片描述

  • key:指向的是键
  • v: 只能使用结构体中的其中一个,val指向的任意值的指针
  • next:指针执行下一个entry

当我们向Dict中添加键值对时,Redis会根据key计算出hash值,然后利用h & sizemask来计算元素应该存储到数组的位置
为什么是h & sizemask
例如size=4,

size:4对应的二进制是... 0000 0100
sizemask:4-1对应的二进制是:... 0000 0011
h & sizemask对应的就是低两位的值:即对h求余的结果
如果h是5 对应的二进制是 ... xxxx x101 那么:h & sizemask =  1

假如我们现在要存储k1=v1,假设k1的哈希值是1,则1 & 3 = 1,因此k1=v1要存储到数组角标为1的位置。首先我们要创建dictEntry数组,默认初始大小为4
在这里插入图片描述
如果这时候又来一个k2=v2,如果k2的hash值求余后也是1,那么这时候存在hash冲突,那么数组角标为1的位置会替换为k2=v2,在新的k2=v2的指针指向旧的数据k1=v1
在这里插入图片描述
为什么要往队首加呢,因为队首加比较方便。队尾加的话,需要遍历前面很多个节点。

2. Dict字典

数据结构如下
在这里插入图片描述

  • type: dict类型,内置不同的hash函数(因为dict在redis中应用场景很多,所以要用到不同的hash函数,所以封装到了dictType当中)
  • ht:哈希表数组,大小为2,其中一个是当前的数据,另一个是空,rehash时使用。
  • rehashidx:rehash的进度,-1表示未进行
  • pauserehash:rehash是否暂停, 1则暂停, 0则继续
    在这里插入图片描述
3. dict的扩容

Dict中的hashtable时结合数组加链表的形式实现的。当hashtable中元素过多,哈希冲突会变多,链表会边长,检索效率会大大降低。
Dict在每次新增元素时,都会检查负载因子,loadFactor = used/size, 满足以下情况时会触发扩容

  • 哈希表的loadFactor >=1, 且服务器没有执行BGSAVE或BGREWRITEAOF等后台进程时(后台进程使CPU是比较高的,且有大量IO读写,可能会影响性能)。
  • 哈希表的loadFactor > 5。(兜底,必须触发扩容了)
    在这里插入图片描述
4.dict的收缩

Dict除了扩容以外,每次删除元素的时候,也会对负载因子做检查,当loadFactor < 0.1,会做哈希表收缩
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5. dict的rehash, 渐进式Hash

不管是扩容还是收缩,必定会创建新的哈希表,导致size和sizemask的变化,而key的查询与sizemask有关。因此必须重新计算key的索引,插入新的hash表中,这个过程成为rehash,过程是这样的:

  • 计算新的hash表realesize,值取决于是做扩容还是收缩:

    1. 如果是扩容,则新的size为第一个大于ht[0].used + 1的2的n次方
    2. 如果是收缩,则新的size为第一个大于ht[0].used的2的n次方,不得小于4
  • 将按照新的realesize,申请内存空间,创建dictht,并赋值给dict.ht[1]

  • 设置rehashidx = 0,表示开始进行rehash

  • 将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]

  • 将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空,释放原来的dict.ht[0]

以下是扩容的过程:
初始为4的dictentry数组,这时来一个新的元素k5=v5在这里插入图片描述
dict.ht[1]扩容为8,将每个元素指到新的数组上
在这里插入图片描述
元素都迁移完后,将dict.ht[1]复制到dict.ht[0],然后清空dict.ht[1]
在这里插入图片描述

渐进式rehash

dict的rehash并不是一次性完成的。试想一下,如果dict中有千万个元素,要在一次的rehash完成,极有可能导致主线程阻塞。因此rehash是分多次,渐进式完成的,因此称为渐进式rehash,流程如下:

  • 计算新的hash表realesize,值取决于是做扩容还是收缩:

    1. 如果是扩容,则新的size为第一个大于ht[0].used + 1的2的n次方
    2. 如果是收缩,则新的size为第一个大于ht[0].used的2的n次方,不得小于4
  • 将按照新的realesize,申请内存空间,创建dictht,并赋值给dict.ht[1]

  • 设置rehashidx = 0,表示开始进行rehash
    - 将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]

  • 每次新增,删除,查询,修改都会检查dict的rehashidx是否大于 -1,如果是则将dict.dict[0].table[rehashidx]的entry链表rehash到dict.ht[1] ,并且将rehashidx ++ ,直到dict.ht[0]的所有数据rehash到dict.ht[1] (查询时会到两个hash表都查询)

  • 将rehashdix变为-1,代表rehash结束

  • 在rehash过程中新增,则写入dict.ht[1],查询,修改,删除则会在dict.ht[0]和dict.ht[1]中依次查找。最后将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空,释放原来的dict.ht[0]

总结
  • 类似java中的HashMap,底层时数组加链表来解决哈希冲突
  • Dict中包含两个哈希表,ht[0]平常用,ht[1]用来rehash
  • 当loadFactor > 1且没有子进程任务或loadFactor > 5,dict会扩容
  • 当loadFactor < 0.1,dict会收缩
  • 扩容大小为第一个大于等于used + 1的2的n次方
  • 收缩大小为第一个大于等于used的2的n次方
  • dict采用渐进式rehash,每次访问Dict会进行一次rehash
  • rehash时ht[0]只增不减,心中操作只在ht[1]执行,其他操作在两个hash表
  • 25
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值