rehash
当哈希表的大小不能满足需求,就可能会有两个或者以上数量的键被分配到了哈希表数组上的同一个索引上,于是就发生冲突(collision),在Redis中解决冲突的办法是链接法(separate chaining)。但是需要尽可能避免冲突,希望哈希表的负载因子(load factor),维持在一个合理的范围之内,就需要对哈希表进行扩展或收缩。
Redis对哈希表的rehash操作步骤如下:
扩展或收缩
扩展:ht[1]的大小为第一个大于等于ht[0].used * 2的 2n2n 。
收缩:ht[1]的大小为第一个大于等于ht[0].used的 2n2n 。
将所有的ht[0]上的节点rehash到ht[1]上。
释放ht[0],将ht[1]设置为第0号表,并创建新的ht[1]。
源码再此:
扩展操作
static int _dictExpandIfNeeded(dict *d) //扩展d字典,并初始化
{
/* Incremental rehashing already in progress. Return. */
if (dictIsRehashing(d)) return DICT_OK; //正在进行rehash,直接返回
/* If the hash table is empty expand it to the initial size. */
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); //如果字典(的 0 号哈希表)为空,那么创建并返回初始化大小的 0 号哈希表
/* If we reached the 1:1 ratio, and we are allowed to resize the hash
* table (global setting) or we should avoid it but the ratio between
* elements/buckets is over the "safe" threshold, we resize doubling
* the number of buckets. */
//1. 字典已使用节点数和字典大小之间的比率接近 1:1
//2. 能够扩展的标志为真
//3. 已使用节点数和字典大小之间的比率超过 dict_force_resize_ratio
if (d->ht[0].used >= d->ht[0].size && (dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
return dictExpand(d, d->ht[0].used*2); //扩展为节点个数的2倍
}
return DICT_OK;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
收缩操作:
int dictResize(dict *d) //缩小字典d
{
int minimal;
//如果dict_can_resize被设置成0,表示不能进行rehash,或正在进行rehash,返回出错标志DICT_ERR
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used; //获得已经有的节点数量作为最小限度minimal
if (minimal < DICT_HT_INITIAL_SIZE)//但是minimal不能小于最低值DICT_HT_INITIAL_SIZE(4)
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal); //用minimal调整字典d的大小
}
1
2
3
4
5
6
7
8
9
10
11
12
扩展和收缩操作都调用了dictExpand()函数,该函数通过计算传入的第二个大小参数进行计算,算出一个最接近2n2n的realsize,然后进行扩展或收缩,dictExpand()函数源码如下:
int dictExpand(dict *d, unsigned long size) //根据size调整或创建字典d的哈希表
{
dictht n; /* the new hash table */
unsigned long realsize = _dictNextPower(size); //获得一个最接近2^n的realsize
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht[0].used > size) //正在rehash或size不够大返回出错标志
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (realsize == d->ht[0].size) return DICT_ERR; //如果新的realsize和原本的size一样则返回出错标志
/* Allocate the new hash table and initialize all pointers to NULL */
//初始化新的哈希表的成员
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0;
/* Is this the first initialization? If so it's not really a rehashing
* we just set the first hash table so that it can accept keys. */
if (d->ht[0].table == NULL) { //如果ht[0]哈希表为空,则将新的哈希表n设置为ht[0]
d->ht[0] = n;
return DICT_OK;
}
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n; //如果ht[0]非空,则需要rehash
d->rehashidx = 0; //设置rehash标志位为0,开始渐进式rehash(incremental rehashing)
return DICT_OK;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
收缩或者扩展哈希表需要将ht[0]表中的所有键全部rehash到ht[1]中,但是rehash操作不是一次性、集中式完成的,而是分多次,渐进式,断续进行的,这样才不会对服务器性能造成影响。因此下面介绍渐进式rehash。
5. 渐进式rehash(incremental rehashing)
渐进式rehash的关键:
字典结构dict中的一个成员rehashidx,当rehashidx为-1时表示不进行rehash,当rehashidx值为0时,表示开始进行rehash。
在rehash期间,每次对字典的添加、删除、查找、或更新操作时,都会判断是否正在进行rehash操作,如果是,则顺带进行单步rehash,并将rehashidx+1。
当rehash时进行完成时,将rehashidx置为-1,表示完成rehash。
源码在此:
static void _dictRehashStep(dict *d) { //单步rehash
if (d->iterators == 0) dictRehash(d,1); //当迭代器数量不为0,才能进行1步rehash
}
int dictRehash(dict *d, int n) { //n步进行rehash
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0; //只有rehashidx不等于-1时,才表示正在进行rehash,否则返回0
while(n-- && d->ht[0].used != 0) { //分n步,而且ht[0]上还有没有移动的节点
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
//确保rehashidx没有越界,因为rehashidx是从-1开始,0表示已经移动1个节点,它总是小于hash表的size的
assert(d->ht[0].size > (unsigned long)d->rehashidx);
//第一个循环用来更新 rehashidx 的值,因为有些桶为空,所以 rehashidx并非每次都比原来前进一个位置,而是有可能前进几个位置,但最多不超过 10。
//将rehashidx移动到ht[0]有节点的下标,也就是table[d->rehashidx]非空
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx]; //ht[0]下标为rehashidx有节点,得到该节点的地址
/* Move all the keys in this bucket from the old to the new hash HT */
//第二个循环用来将ht[0]表中每次找到的非空桶中的链表(或者就是单个节点)拷贝到ht[1]中
while(de) {
unsigned int h;
nextde = de->next; //备份下一个节点的地址
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & d->ht[1].sizemask; //获得计算哈希值并得到哈希表中的下标h
//将该节点插入到下标为h的位置
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
//更新两个表节点数目计数器
d->ht[0].used--;
d->ht[1].used++;
//将de指向以一个处理的节点
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL; //迁移过后将该下标的指针置为空
d->rehashidx++; //更新rehashidx
}
/* Check if we already rehashed the whole table... */
if (d->ht[0].used == 0) { //ht[0]上已经没有节点了,说明已经迁移完成
zfree(d->ht[0].table); //释放hash表内存
d->ht[0] = d->ht[1]; //将迁移过的1号哈希表设置为0号哈希表
_dictReset(&d->ht[1]); //重置ht[1]哈希表
d->rehashidx = -1; //rehash标志关闭
return 0; //表示前已完成
}
/* More to rehash... */
return 1; //表示还有节点等待迁移
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
6. 迭代器
redis在字典结构也定义了迭代器
typedef struct dictIterator {
dict *d; //被迭代的字典
long index; //迭代器当前所指向的哈希表索引位置
int table, safe; //table表示正迭代的哈希表号码,ht[0]或ht[1]。safe表示这个迭代器是否安全。
dictEntry *entry, *nextEntry; //entry指向当前迭代的哈希表节点,nextEntry则指向当前节点的下一个节点。
/* unsafe iterator fingerprint for misuse detection. */
long long fingerprint; //避免不安全迭代器的指纹标记
} dictIterator;
1
2
3
4
5
6
7
8
迭代器分为安全迭代器和不安全迭代器:
非安全迭代器只能进行Get等读的操作, 而安全迭代器则可以进行iterator支持的任何操作。
由于dict结构中保存了safe iterators的数量,如果数量不为0, 是不能进行下一步的rehash的; 因此安全迭代器的存在保证了遍历数据的准确性。
在非安全迭代器的迭代过程中, 会通过fingerprint方法来校验iterator在初始化与释放时字典的hash值是否一致; 如果不一致说明迭代过程中发生了非法操作.
关于dictScan()反向二进制迭代器的原理介绍:Scan迭代器遍历操作原理
当哈希表的大小不能满足需求,就可能会有两个或者以上数量的键被分配到了哈希表数组上的同一个索引上,于是就发生冲突(collision),在Redis中解决冲突的办法是链接法(separate chaining)。但是需要尽可能避免冲突,希望哈希表的负载因子(load factor),维持在一个合理的范围之内,就需要对哈希表进行扩展或收缩。
Redis对哈希表的rehash操作步骤如下:
扩展或收缩
扩展:ht[1]的大小为第一个大于等于ht[0].used * 2的 2n2n 。
收缩:ht[1]的大小为第一个大于等于ht[0].used的 2n2n 。
将所有的ht[0]上的节点rehash到ht[1]上。
释放ht[0],将ht[1]设置为第0号表,并创建新的ht[1]。
源码再此:
扩展操作
static int _dictExpandIfNeeded(dict *d) //扩展d字典,并初始化
{
/* Incremental rehashing already in progress. Return. */
if (dictIsRehashing(d)) return DICT_OK; //正在进行rehash,直接返回
/* If the hash table is empty expand it to the initial size. */
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); //如果字典(的 0 号哈希表)为空,那么创建并返回初始化大小的 0 号哈希表
/* If we reached the 1:1 ratio, and we are allowed to resize the hash
* table (global setting) or we should avoid it but the ratio between
* elements/buckets is over the "safe" threshold, we resize doubling
* the number of buckets. */
//1. 字典已使用节点数和字典大小之间的比率接近 1:1
//2. 能够扩展的标志为真
//3. 已使用节点数和字典大小之间的比率超过 dict_force_resize_ratio
if (d->ht[0].used >= d->ht[0].size && (dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
return dictExpand(d, d->ht[0].used*2); //扩展为节点个数的2倍
}
return DICT_OK;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
收缩操作:
int dictResize(dict *d) //缩小字典d
{
int minimal;
//如果dict_can_resize被设置成0,表示不能进行rehash,或正在进行rehash,返回出错标志DICT_ERR
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used; //获得已经有的节点数量作为最小限度minimal
if (minimal < DICT_HT_INITIAL_SIZE)//但是minimal不能小于最低值DICT_HT_INITIAL_SIZE(4)
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal); //用minimal调整字典d的大小
}
1
2
3
4
5
6
7
8
9
10
11
12
扩展和收缩操作都调用了dictExpand()函数,该函数通过计算传入的第二个大小参数进行计算,算出一个最接近2n2n的realsize,然后进行扩展或收缩,dictExpand()函数源码如下:
int dictExpand(dict *d, unsigned long size) //根据size调整或创建字典d的哈希表
{
dictht n; /* the new hash table */
unsigned long realsize = _dictNextPower(size); //获得一个最接近2^n的realsize
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht[0].used > size) //正在rehash或size不够大返回出错标志
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (realsize == d->ht[0].size) return DICT_ERR; //如果新的realsize和原本的size一样则返回出错标志
/* Allocate the new hash table and initialize all pointers to NULL */
//初始化新的哈希表的成员
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0;
/* Is this the first initialization? If so it's not really a rehashing
* we just set the first hash table so that it can accept keys. */
if (d->ht[0].table == NULL) { //如果ht[0]哈希表为空,则将新的哈希表n设置为ht[0]
d->ht[0] = n;
return DICT_OK;
}
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n; //如果ht[0]非空,则需要rehash
d->rehashidx = 0; //设置rehash标志位为0,开始渐进式rehash(incremental rehashing)
return DICT_OK;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
收缩或者扩展哈希表需要将ht[0]表中的所有键全部rehash到ht[1]中,但是rehash操作不是一次性、集中式完成的,而是分多次,渐进式,断续进行的,这样才不会对服务器性能造成影响。因此下面介绍渐进式rehash。
5. 渐进式rehash(incremental rehashing)
渐进式rehash的关键:
字典结构dict中的一个成员rehashidx,当rehashidx为-1时表示不进行rehash,当rehashidx值为0时,表示开始进行rehash。
在rehash期间,每次对字典的添加、删除、查找、或更新操作时,都会判断是否正在进行rehash操作,如果是,则顺带进行单步rehash,并将rehashidx+1。
当rehash时进行完成时,将rehashidx置为-1,表示完成rehash。
源码在此:
static void _dictRehashStep(dict *d) { //单步rehash
if (d->iterators == 0) dictRehash(d,1); //当迭代器数量不为0,才能进行1步rehash
}
int dictRehash(dict *d, int n) { //n步进行rehash
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0; //只有rehashidx不等于-1时,才表示正在进行rehash,否则返回0
while(n-- && d->ht[0].used != 0) { //分n步,而且ht[0]上还有没有移动的节点
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
//确保rehashidx没有越界,因为rehashidx是从-1开始,0表示已经移动1个节点,它总是小于hash表的size的
assert(d->ht[0].size > (unsigned long)d->rehashidx);
//第一个循环用来更新 rehashidx 的值,因为有些桶为空,所以 rehashidx并非每次都比原来前进一个位置,而是有可能前进几个位置,但最多不超过 10。
//将rehashidx移动到ht[0]有节点的下标,也就是table[d->rehashidx]非空
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx]; //ht[0]下标为rehashidx有节点,得到该节点的地址
/* Move all the keys in this bucket from the old to the new hash HT */
//第二个循环用来将ht[0]表中每次找到的非空桶中的链表(或者就是单个节点)拷贝到ht[1]中
while(de) {
unsigned int h;
nextde = de->next; //备份下一个节点的地址
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & d->ht[1].sizemask; //获得计算哈希值并得到哈希表中的下标h
//将该节点插入到下标为h的位置
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
//更新两个表节点数目计数器
d->ht[0].used--;
d->ht[1].used++;
//将de指向以一个处理的节点
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL; //迁移过后将该下标的指针置为空
d->rehashidx++; //更新rehashidx
}
/* Check if we already rehashed the whole table... */
if (d->ht[0].used == 0) { //ht[0]上已经没有节点了,说明已经迁移完成
zfree(d->ht[0].table); //释放hash表内存
d->ht[0] = d->ht[1]; //将迁移过的1号哈希表设置为0号哈希表
_dictReset(&d->ht[1]); //重置ht[1]哈希表
d->rehashidx = -1; //rehash标志关闭
return 0; //表示前已完成
}
/* More to rehash... */
return 1; //表示还有节点等待迁移
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
6. 迭代器
redis在字典结构也定义了迭代器
typedef struct dictIterator {
dict *d; //被迭代的字典
long index; //迭代器当前所指向的哈希表索引位置
int table, safe; //table表示正迭代的哈希表号码,ht[0]或ht[1]。safe表示这个迭代器是否安全。
dictEntry *entry, *nextEntry; //entry指向当前迭代的哈希表节点,nextEntry则指向当前节点的下一个节点。
/* unsafe iterator fingerprint for misuse detection. */
long long fingerprint; //避免不安全迭代器的指纹标记
} dictIterator;
1
2
3
4
5
6
7
8
迭代器分为安全迭代器和不安全迭代器:
非安全迭代器只能进行Get等读的操作, 而安全迭代器则可以进行iterator支持的任何操作。
由于dict结构中保存了safe iterators的数量,如果数量不为0, 是不能进行下一步的rehash的; 因此安全迭代器的存在保证了遍历数据的准确性。
在非安全迭代器的迭代过程中, 会通过fingerprint方法来校验iterator在初始化与释放时字典的hash值是否一致; 如果不一致说明迭代过程中发生了非法操作.
关于dictScan()反向二进制迭代器的原理介绍:Scan迭代器遍历操作原理