Redisbook学习笔记(1)字典(3)

渐进式rehashnode

在上一节,咱们了解了字典的rehash 过程,须要特别指出的是,rehash 程序并非在激活之redis

后就立刻执行直到完成的,而是分屡次、渐进式地完成的。算法

假设这样一个场景:在一个有不少键值对的字典里,某个用户在添加新键值对时触发了rehash数据库

过程,若是这个rehash 过程必须将全部键值对迁移完毕以后才将结果返回给用户,这样的处理数组

方式将是很是不友好的。安全

另外一方面,要求服务器必须阻塞直到rehash 完成,这对于Redis 服务器自己也是不能接受的。服务器

为了解决这个问题,Redis 使用了渐进式(incremental)的rehash 方式:经过将rehash 分散数据结构

到多个步骤中进行,从而避免了集中式的计算。ide

渐进式rehash 主要由_dictRehashStep 和dictRehashMilliseconds 两个函数进行:
. _dictRehashStep 用于对数据库字典、以及哈希键的字典进行被动rehash ;
. dictRehashMilliseconds 则由Redis 服务器常规任务程序(server cron job)执行,用
于对数据库字典进行主动rehash ;
_dictRehashStep
每次执行_dictRehashStep ,ht[0]->table 哈希表第一个不为空的索引上的全部节点就会全
部迁移到ht[1]->table 。函数

在rehash 开始进行以后(d->rehashidx 不为-1),每次执行一次添加、查找、删除操做,
_dictRehashStep 都会被执行一次:

wKiom1Lfx8-xIvSUAAFG-ophSV4572.jpg

由于字典会保持哈希表大小和节点数的比率在一个很小的范围内,因此每一个索引上的节点数量
不会不少(从目前版本的rehash 条件来看,平均只有一个,最多一般也不会超过五个),因此
在执行操做的同时,对单个索引上的节点进行迁移,几乎不会对响应时间形成影响。
dictRehashMilliseconds
dictRehashMilliseconds 能够在指定的毫秒数内,对字典进行rehash 。
当Redis 的服务器常规任务执行时,dictRehashMilliseconds 会被执行,在规定的时间内,
尽量地对数据库字典中那些须要rehash 的字典进行rehash ,从而加速数据库字典的rehash
进程(progress)。
其余措施
在哈希表进行rehash 时,字典还会采起一些特别的措施,确保rehash 顺利、正确地进行:
 由于在rehash 时,字典会同时使用两个哈希表,因此在这期间的全部查找、删除等操做,
除了在ht[0] 上进行,还须要在ht[1] 上进行。
 在执行添加操做时,新的节点会直接添加到ht[1] 而不是ht[0] ,这样保证ht[0] 的节
点数量在整个rehash 过程当中都只减不增。

字典的收缩
上面关于rehash 的章节描述了经过rehash 对字典进行扩展(expand)的状况,若是哈希表的
可用节点数比已用节点数大不少的话,那么也能够经过对哈希表进行rehash 来收缩(shrink)
字典。
收缩rehash 和上面展现的扩展rehash 的操做几乎同样,它执行如下步骤:
1. 建立一个比ht[0]->table 小的ht[1]->table ;
2. 将ht[0]->table 中的全部键值对迁移到ht[1]->table ;
3. 将原有ht[0] 的数据清空,并将ht[1] 替换为新的ht[0] ;
扩展rehash 和收缩rehash 执行彻底相同的过程,一个rehash 是扩展仍是收缩字典,关键在于
新分配的ht[1]->table 的大小:
. 若是rehash 是扩展操做,那么ht[1]->table 比ht[0]->table 要大;
. 若是rehash 是收缩操做,那么ht[1]->table 比ht[0]->table 要小;
字典的收缩规则由redis.c/htNeedsResize 函数定义:

/*
* 检查字典的使用率是否低于系统容许的最小比率
**
是的话返回1 ,不然返回0 。
*/
int htNeedsResize(dict *dict) {
long long size, used;
// 哈希表已用节点数量
size = dictSlots(dict);
// 哈希表大小
used = dictSize(dict);
// 当哈希表的大小大于DICT_HT_INITIAL_SIZE
// 而且字典的填充率低于REDIS_HT_MINFILL 时
// 返回1
return (size && used && size > DICT_HT_INITIAL_SIZE &&
(used*100/size < REDIS_HT_MINFILL));
}

在默认状况下,REDIS_HT_MINFILL 的值为10 ,也便是说,当字典的填充率低于10% 时,程
序就能够对这个字典进行收缩操做了。
字典收缩和字典扩展的一个区别是:
. 字典的扩展操做是自动触发的(不论是自动扩展仍是强制扩展);
. 而字典的收缩操做则是由程序手动执行。
所以,使用字典的程序能够决定什么时候对字典进行收缩:
. 当字典用于实现哈希键的时候,每次从字典中删除一个键值对,程序就会执行一次
htNeedsResize 函数,若是字典达到了收缩的标准,程序将当即对字典进行收缩;
. 当字典用于实现数据库键空间(key space) 的时候, 收缩的时机由
redis.c/tryResizeHashTables 函数决定.

字典其余操做
除了添加操做和伸展/收缩操做以外,字典还定义了其余一些操做,好比常见的查找、删除和更
新。
由于链地址法哈希表实现的相关信息能够从任何一本数据结构或算法书上找到,这里再也不对字
典的其余操做进行介绍,不过前面对建立字典、添加键值对、收缩和扩展rehash 的讨论已经涵
盖了字典模块的核心内容。

字典的迭代
字典带有本身的迭代器实现——对字典进行迭代实际上就是对字典所使用的哈希表进行迭代:
. 迭代器首先迭代字典的第一个哈希表,而后,若是rehash 正在进行的话,就继续对第二
个哈希表进行迭代。
. 当迭代哈希表时,找到第一个不为空的索引,而后迭代这个索引上的全部节点。
. 当这个索引迭代完了,继续查找下一个不为空的索引,如此循环,一直到整个哈希表都迭
代完为止。
整个迭代过程能够用伪代码表示以下:

def iter_dict(dict):
// 迭代0 号哈希表
iter_table(ht[0]->table)
// 若是正在执行rehash ,那么也迭代1 号哈希表
if dict.is_rehashing(): iter_table(ht[1]->table)
def iter_table(table):
// 遍历哈希表上的全部索引
for index in table:
// 跳过空索引
if table[index].empty():
continue
// 遍历索引上的全部节点
for node in table[index]:
// 处理节点
do_something_with(node)

字典的迭代器有两种:
 安全迭代器:在迭代进行过程当中,能够对字典进行修改。
 不安全迭代器:在迭代进行过程当中,不对字典进行修改。
如下是迭代器的数据结构定义:

/*
* 字典迭代器
*/
typedef struct dictIterator {
dict *d; // 正在迭代的字典
int table, // 正在迭代的哈希表的号码(0 或者1)
index, // 正在迭代的哈希表数组的索引
safe; // 是否安全?
dictEntry *entry, // 当前哈希节点
*nextEntry; // 当前哈希节点的后继节点
} dictIterator;

小结 字典由键值对构成的抽象数据结构。 Redis 中的数据库和哈希键都基于字典来实现。 Redis 字典的底层实现为哈希表,每一个字典使用两个哈希表,通常状况下只使用0 号哈希表,只有在rehash 进行时,才会同时使用0 号和1 号哈希表。 哈希表使用链地址法来解决键冲突的问题。 Rehash 能够用于扩展或收缩哈希表。 对哈希表的rehash 是分屡次、渐进式地进行的。

相关文章
相关标签/搜索