扑街前言:上篇文章内容讲了Redis的key-value是如何存储的,那么本次就说一下如果key值经过HashFunction后得到的数字重复了怎么办?也就是哈希冲突了该怎么解决,以及详细一下其中一种解决方案拉链法。(认识到自己是菜鸟的第二天)
继上篇文章说的,Hash table是基础数组构建的,Redis通过HashFunction(key)传入一个key,返回一个数字,这个数字也就是Hash table的下标索引,那么如果key不同的情况下却返回了同一个数字,按照下标索引相同的情况处理的话,就会导致上一个值被覆盖,但是两个key的原值又是不一样的,所以就出现了哈希冲突(也称为哈希碰撞)。而解决哈希冲突,我了解的两种方法:开放寻址法、拉链发。
开放寻址发:
又称开放定址法,当哈希冲突发生时,从发生冲突的那个单元起,按照一定的次序,从哈希表中寻找一个空闲的单元,然后把发生冲突的元素存入到该单元。这个空闲单元又称为开放单元或者空白单元。开放寻址法需要的表长度要大于等于所需要存放的元素数量,非常适用于装载因子较小(小于0.5)的散列表。但是不适用于Redis中的问题,因为在此取值时,无法拿到key的哈希值。开放寻址发类似找停车位:
拉链法:
这种方法关键是把数组的同一个下标对应的槽位拉出一个链表,然后把所有的元素放在这个链表中。以Redis的哈希字典为例(如下截图)
拉链法相对于开发寻址法的优缺点:
优点:
- 拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
- 由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
- 开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
- 在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
缺点:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
红黑树,Java1.8后,Java的hashMap存值就是链表加红黑树,而链表转红黑树的条件:数组长度大于64,并且某个链表的节点数大于8。当某个链表节点数小于6的时候,红黑树转链表。
装载因子的概念:
上篇文章有说Redis的哈希表中的used / size = 装载因子(也就是节点数量 / 数组大小),而这个装载因子如果达到某个数字,哈希表就会扩容,也就是使用1号哈希表,原本在0号哈希表的内容将迁移至1号哈希表,这个过程就是rehash,而当全部数据迁移完成之后,会释放0号哈希表,将0号指针指向1号哈希表,1号哈希表指向空,其中Redis代码提供的方法distRehashStep迁移函数。
rehash:
Redis在rehash时,运行函数 _dictRehashStep,如果目前的哈希字段的正在进行遍历的iterator的个数为0,那么就进行dictRehash,而dictRehash函数的目的就是通过0号哈希表的内容复制增加到1号哈希中,然后修改0号和1号哈希表的大小,并更新哈希字典rehashidx等信息(表示数据正在rehash,这个可以看出Redis的rehash并不是一次完成的)
Redis的扩容迁移是增量式/渐进式迁移,并不是一次性就将所有数据迁移完成,而是分为不同的请求逐步操作,用哈希字典的rehashidx字段来判断标记,并且每次请求操作也不是一直去查询所有数据,如果rehashidx记录的后一位是null,那么就会顺延查询10位,如果10位后还是没有数据,那么就会结束本次请求。当所有数据迁移完成之后,会释放0号哈希表,将0号指针指向1号哈希表,1号哈希表指向空。
static void _dictRehashStep(dict *d) {
// 如果当前正在进行遍历的iterator的个数 = 0,就进行dictRehash
if (d->iterators == 0) dictRehash(d,1);
}
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(d->ht[0].size > (unsigned long)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];
/* Move all the keys in this bucket from the old to the new hash HT */
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;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
Redis的扩容:
rehash的前提是已近存在了1号哈希表,如何去获取1号哈希表,Redis也提供出了对应的函数dictExpand,首先根据_dictNextPower函数计算真实的大小,即需要扩容的大小,DICT_HT_INITIAL_SIZE常量的默认值为4,计算扩容大小时,也是计算4的2倍大小,限制是大于等于需要扩容的大小。扩容本质就是创建一个新的hash table(也就是1号哈希表)。
#define DICT_HT_INITIAL_SIZE 4
/* Our hash table capability is a power of two */
static unsigned long _dictNextPower(unsigned long size)
{
unsigned long i = DICT_HT_INITIAL_SIZE;
if (size >= LONG_MAX) return LONG_MAX;
while(1) {
if (i >= size)
return i;
i *= 2;
}
}
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
dictht n; /* the new hash table */
unsigned long realsize = _dictNextPower(size);
/* 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)
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (realsize == d->ht[0].size) return DICT_ERR;
/* 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) {
d->ht[0] = n;
return DICT_OK;
}
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
总结:
Redis的扩容迁移总的来说,就是当目前0号哈希表的容量不够存值时,创建一个4的2*n倍的1号哈希表,然后逐步迁移0号哈希表的数据,当0号哈希表全部数据迁移完成之后,将0号哈希表释放,然后1号哈希表指向0号哈希表,就此结束。