Redis设计与实现第四章笔记
date
Jul 17, 2021
slug
Redis设计与实现第四章笔记
status
Published
tags
读书笔记
summary
字典
type
Post
4.1 字典的实现
使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对
4.1.1 哈希表
举例:
4.1.2 哈希表节点
key属性保存着键值对中的键,v属性则保存着键值对中的值,值可以是指针,或者一个uint64_t整数,或是一个int64_t整数。next属性是指向另一个节点的指针,用于解决哈希冲突
4.1.3 字典
type和privatedata是针对不同类型的键值对,为创建多态字典而设置的
- type:一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数。
- privatedata保存了需要传给那些类型特定函数的可选参数
ht属性的每一项都是一个dictht哈希表,一般情况下,字典只使用ht[0],而ht[1]只会在对ht[0]进行rehash时使用。
一个没有进行rehash的字典:
4.2 哈希算法
Redis计算哈希值和索引值的方法:
# 使用字典设置的哈希函数,计算键key的哈希值
hash = dict -> type -> hashFunction(key)
# 使用哈希表的sizemask属性和哈希值,计算出索引值
# 根据情况不同,ht[x]可以是ht[0]或者ht[1]
index = hash & dict -> ht[x]].sizemask
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值
4.3 解决键冲突
Redis哈希表采用链地址法解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表。
为了速度考虑,程序总是将新节点添加到链表的表头位置,复杂度为O(1)。
4.4 rehash
为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要通过rehash对哈希表的大小进行相应的扩展或者收缩。
rehash步骤:
- 为字典的ht[1]哈希表分配空间,空间大小取决于要执行的操作以及ht[0]当前包含的键值对数量。
- 如果时扩展,那么ht[1]的大小为第一个大小等于ht[0].used * 2的 2^n
- 如果执行的时收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2^n
- 将保存在ht[0]中的所有键值对rehash到ht[1]上面,rehash指重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上
- 当ht[0]包含的所有键值对都迁移到了ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备
哈希表的扩展与收缩
当任一条件满足,会自动开始对哈希表进行扩展操作:
- 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1
- 服务器目前正在执行BGSAVE或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5
负载因子计算:
load_factor = ht[0].used / ht[0].size
上述两个命令是否在执行的负载因子不同,是因为避免在上述命令执行时,又同时执行rehash,造成不必要的内存写入操作。
当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作
4.5 渐进式rehash
rehash并不是一次性、集中式完成的,而是分多次、渐进式完成的。
渐进式rehash的步骤:
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
- 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作开始
- 在rehash期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成后,程序将rehashidx属性的值+1
- 随着不断进行,最终在某个时间,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示操作已完成
渐进式rehash执行期间的哈希表操作
rehash过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash期间,字典的CRUD同样会用两个哈希表。在此期间,新添加到字典的键值对一律会被保存到ht[1]里面,ht[0]不再进行任何添加操作