Redis Rehash专题

dict数据结构

dict是一个用于维护key和value映射关系的数据结构,与很多语言中的Map或dictionary类似。Redis的一个database中所有key到value的映射,就是使用一个dict来维护的。dict本质上是为了解决算法中的查找问题(Searching)。

1.一般查找问题的解法分为两个大类:一个是基于各种平衡树,一个是基于哈希表,平常使用的各种Map或dictionary,大都是基于哈希表实现的。

2.dict也是一个基于哈希表的算法,跟java中的hashMap类似,用key计算出哈希值,并得到key在哈希表中的位置,再采用拉链法解决冲突,并在装载因子(load factor)超过预定值时自动扩展内存,引发重哈希(rehashing)。

为了能更清楚地展示dict的数据结构定义,用一张结构图来表示dict(字典)的构成。如下图:

#dict字典的数据结构
typedef struct dict{
    dictType *type; //直线dictType结构,dictType结构中包含自定义的函数,这些函数使得key和value能够存储任何类型的数据
    void *privdata; //私有数据,保存着dictType结构中函数的 参数
    dictht ht[2]; //两张哈希表
    long rehashidx; //rehash的标记,rehashidx=-1表示没有进行rehash,rehash时每迁移一个桶就对rehashidx加一
    int itreators;  //正在迭代的迭代器数量
}
 
#dict结构中ht[0]、ht[1]哈希表的数据结构
typedef struct dictht{
    dictEntry[] table;        //存放一个数组的地址,数组中存放哈希节点dictEntry的地址
    unsingned long size;      //哈希表table的大小,出始大小为4
    unsingned long  sizemask; //用于将hash值映射到table位置的索引,大小为(size-1)
    unsingned long  used;     //记录哈希表已有节点(键值对)的数量
}

 

上图就是Redis的dict(字典)数据结构,一个dict需要注意几点:

(1)dict采用哈希函数对key取哈希值得到在哈希表中的位置(桶的位置),采用拉链法解决hash冲突

(2)两张哈希表(ht[2]):只有在重哈希的过程中,ht[0]和ht[1]才都有效。而在平常情况下,只有ht[0]有效,ht[1]里面没有任何数据。上图表示的就是重哈希进行到中间某一步时的情况。

(3)重哈希:跟HashMap一样当装载因子(load factor)超过预定值时就会进行rehash。dict进行rehash扩容,将ht[0]上某一个bucket(即一个桶上dictEntry链表)上的每一个链表移动到扩容后的ht[1]上(每次只移动一个链表,即渐进式rehash。原因是为了防止redis长时间的堵塞导致不可用),触发rehash的操作有查询、插入和删除元素。rehashidx会记录每次需要移动链表bucket桶的位置(后面会详细讲解)。

(4)当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表),释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。

触发rehash的条件

(1)负载因子计算

#哈希表的负载因子计算:负载因子 = 哈希表已保存节点数量 / 哈希表大小

load_factor = ht[0].used / ht[0].size

(2)rehash的条件

触发dict的rehash主要有两种:一种是触发扩容操作,另一种是触发收缩操作。两种rehash触发的条件是不一样的,需要各自满足一下条件才能导致rehash操作。

1)触发扩容操作条件

当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作。

1、服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1。

2、服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 5。

根据 BGSAVE 命令或 BGREWRITEAOF 命令是否正在执行,服务器执行扩展操作所需的负载因子并不相同,这是因为在执行 BGSAVE 命令或 BGREWRITEAOF命令的过程中, Redis会fork一个子进程,而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,这可以避免不必要的内存写入操作, 最大限度地节约内存。

2)触发收缩操作条件:

当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。

(ht[0].used / ht[0].siz) < 0.1,也就是填充率必须<10%。

 

dict添加、查询和删除操作

(1)dict添加操作:如果正在重哈希中,会把数据插入到ht[1];否则插入到ht[0]。

(2)dict查询操作:先在第一个哈希表ht[0]上进行查找,再判断当前是否在重哈希,如果没有,那么在ht[0]上的查找结果就是最终结果。否则,在ht[1]上进行查找。查询时会先根据key计算出桶的位置,在到桶里的链表上寻找key。

(3)dict删除操作:判断当前是不是在重哈希过程中,如果是只在ht[0]中查找要删除的key;否则ht[0]和ht[1]它都要查找删除。

渐进式 rehash

扩展或收缩哈希表需要将 ht[0]里面的所有键值对 rehash 到 ht[1]里面, 但是, 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的

这样做的原因在于,如果哈希表里保存的键值对数量很大时, 如:四百万、四千万甚至四亿个键值对, 那么一次性将这些键值对全部 rehash 到 ht[1] 的话,庞大的计算量(需要重新计算链表在桶中的位置)可能会导致服务器在一段时间内停止服务(redis是单线程的,如果全部移动会引起客户端长时间阻塞不可用)

因此, 为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0]里面的所有键值对全部 rehash 到 ht[1], 而是分多次、渐进式地将 ht[0]里面的键值对慢慢地 rehash 到 ht[1]

以下是哈希表渐进式rehash的详细步骤:

(1)为ht[1]分配空间,让dict字典同时持有 ht[0] 和 ht[1] 两个哈希表。

(2)在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。

(3)在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在 rehashidx索引(table[rehashidx]桶上的链表)上的所有键值对rehash到ht[1]上,当rehash工作完成之后,将rehashidx属性的值增一,表示下一次要迁移链表所在桶的位置

(4)随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有桶对应的键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成

渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。

总结:渐进式 rehash 执行期间的哈希表操作

(1)删除和查找:在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作会在两个哈希表上进行。比如说,要在字典里面查找一个键的话,程序会先在ht[0]里面进行查找,如果没找到的话,就会继续到ht[1]里面进行查找,诸如此类。

(2)新增数据:在渐进式 rehash 执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作。这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值