gitbook/Redis源码剖析与实战/docs/400379.md
2022-09-03 22:05:03 +08:00

397 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 03 | 如何实现一个性能优异的Hash表?
你好,我是蒋德钧。今天,我们来聊聊Redis中的Hash。
我们知道,Hash表是一种非常关键的数据结构,在计算机系统中发挥着重要作用。比如在Memcached中,Hash表被用来索引数据;在数据库系统中,Hash表被用来辅助SQL查询。而对于Redis键值数据库来说,Hash表既是键值对中的一种值类型,同时,Redis也使用一个全局Hash表来保存所有的键值对,从而既满足应用存取Hash结构数据需求,又能提供快速查询功能。
那么,Hash表应用如此广泛的一个重要原因,就是从理论上来说,它能以O(1)的复杂度快速查询数据。Hash表通过Hash函数的计算,就能定位数据在表中的位置,紧接着可以对数据进行操作,这就使得数据操作非常快速。
Hash表这个结构也并不难理解,但是在实际应用Hash表时,当数据量不断增加,它的性能就经常会受到**哈希冲突**和**rehash开销**的影响。而这两个问题的核心,其实都来自于Hash表要保存的数据量,超过了当前Hash表能容纳的数据量。
那么要如何应对这两个问题呢?事实上,这也是在大厂面试中,面试官经常会考核的问题。所以你现在可以先想想,如果你在面试中遇到了这两个问题,你会怎么回答呢?
OK,思考先到这里,现在我来告诉你Redis是怎么很好地解决这两个问题的。
Redis为我们提供了一个经典的Hash表实现方案。针对哈希冲突,Redis采用了**链式哈希**,在不扩容哈希表的前提下,将具有相同哈希值的数据链接起来,以便这些数据在表中仍然可以被查询到;对于rehash开销,Redis实现了**渐进式rehash设计**,进而缓解了rehash操作带来的额外开销对系统的性能影响。
所以这节课,我就带你来学习Redis中针对Hash表的设计思路和实现方法,帮助你掌握应对哈希冲突和优化rehash操作性能的能力,并以此支撑你在实际使用Hash表保存大量数据的场景中,可以实现高性能的Hash表。
好了,接下来,我们就先来聊聊链式哈希的设计与实现。
## Redis如何实现链式哈希?
不过,在开始学习链式哈希的设计实现之前,我们还需要明白Redis中Hash表的结构设计是啥样的,以及为何会在数据量增加时产生哈希冲突,这样也更容易帮助我们理解链式哈希应对哈希冲突的解决思路。
### 什么是哈希冲突?
实际上,一个最简单的Hash表就是一个数组,数组里的每个元素是一个哈希桶(也叫做Bucket),第一个数组元素被编为哈希桶0,以此类推。当一个键值对的键经过Hash函数计算后,再对数组元素个数取模,就能得到该键值对对应的数组元素位置,也就是第几个哈希桶。
如下图所示,key1经过哈希计算和哈希值取模后,就对应哈希桶1,类似的,key3和key16分别对应哈希桶7和桶4。
![](https://static001.geekbang.org/resource/image/08/2f/08ac157a8fbf4d22f8a5217bfea79a2f.jpg?wh=2000x1029)
从图上我们还可以看到,需要写入Hash表的键空间一共有16个键,而Hash表的空间大小只有8个元素,这样就会导致有些键会对应到相同的哈希桶中。
我们在实际应用Hash表时,其实一般很难预估要保存的数据量,如果我们一开始就创建一个非常大的哈希表,当数据量较小时,就会造成空间浪费。所以,我们通常会给哈希表设定一个初始大小,而当数据量增加时,键空间的大小就会大于Hash表空间大小了。
也正是由于键空间会大于Hash表空间,这就导致在用Hash函数把键映射到Hash表空间时,不可避免地会出现不同的键被映射到数组的同一个位置上。而如果同一个位置只能保存一个键值对,就会导致Hash表保存的数据非常有限,这就是我们常说的**哈希冲突**。
比如下图中,key3和key100都被映射到了Hash表的桶5中,这样,当桶5只能保存一个key时,key3和key100就会有一个key无法保存到哈希表中了。
![](https://static001.geekbang.org/resource/image/04/d8/04322775d11cea97049bcd4dd8109bd8.jpg?wh=2000x889)
那么我们该如何解决哈希冲突呢?可以考虑使用以下两种解决方案:
* 第一种方案,就是我接下来要给你介绍的**链式哈希**。这里你需要先知道,链式哈希的链不能太长,否则会降低Hash表性能。
* 第二种方案,就是当链式哈希的链长达到一定长度时,我们可以使用**rehash**。不过,执行rehash本身开销比较大,所以就需要采用我稍后会给你介绍的渐进式rehash设计。
这里,我们先来了解链式哈希的设计和实现。
### 链式哈希如何设计与实现?
所谓的链式哈希,就是**用一个链表把映射到Hash表同一桶中的键给连接起来**。下面我们就来看看Redis是如何实现链式哈希的,以及为何链式哈希能够帮助解决哈希冲突。
首先,我们需要了解Redis源码中对Hash表的实现。Redis中和Hash表实现相关的文件主要是**dict.h**和**dict.c**。其中,dict.h文件定义了Hash表的结构、哈希项,以及Hash表的各种操作函数,而dict.c文件包含了Hash表各种操作的具体实现代码。
在dict.h文件中,Hash表被定义为一个二维数组(dictEntry \*\*table),这个数组的每个元素是一个指向哈希项(dictEntry)的指针。下面的代码展示的就是在dict.h文件中对Hash表的定义,你可以看下:
```
typedef struct dictht {
dictEntry **table; //二维数组
unsigned long size; //Hash表大小
unsigned long sizemask;
unsigned long used;
} dictht;
```
那么为了实现链式哈希, Redis在每个dictEntry的结构设计中,**除了包含指向键和值的指针,还包含了指向下一个哈希项的指针**。如下面的代码所示,dictEntry结构体中包含了指向另一个dictEntry结构的**指针\*next**,这就是用来实现链式哈希的:
```
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
```
除了用于实现链式哈希的指针外,这里还有一个值得注意的地方,就是在dictEntry结构体中,键值对的值是由一个**联合体v**定义的。这个联合体v中包含了指向实际值的指针\*val,还包含了无符号的64位整数、有符号的64位整数,以及double类的值。
我之所以要提醒你注意这里,其实是为了说明,**这种实现方法是一种节省内存的开发小技巧**,非常值得学习。因为当值为整数或双精度浮点数时,由于其本身就是64位,就可以不用指针指向了,而是可以直接存在键值对的结构体中,这样就避免了再用一个指针,从而节省了内存空间。
好了,那么到这里,你应该就了解了Redis中链式哈希的实现,不过现在你可能还是不太明白,为什么这种链式哈希可以帮助解决哈希冲突呢?
别着急,我就拿刚才的例子来说明一下,key3和key100都被映射到了Hash表的桶5中。而当使用了链式哈希,桶5就不会只保存key3或key100,而是会用一个链表把key3和key100连接起来,如下图所示。当有更多的key被映射到桶5时,这些key都可以用链表串接起来,以应对哈希冲突。
![](https://static001.geekbang.org/resource/image/28/85/281919546bb9cf97cd70718072389585.jpg?wh=2000x866)
这样,当我们要查询key100时,可以先通过哈希函数计算,得到key100的哈希值被映射到了桶5中。然后,我们再逐一比较桶5中串接的key,直到查找到key100。如此一来,我们就能在链式哈希中找到所查的哈希项了。
不过,链式哈希也存在局限性,那就是随着链表长度的增加,Hash表在一个位置上查询哈希项的耗时就会增加,从而增加了Hash表的整体查询时间,这样也会导致Hash表的性能下降。
那么,有没有什么其他的方法可以减少对Hash表性能的影响呢?当然是有的,这就是接下来我要给你介绍的rehash的设计与实现了。
## Redis如何实现rehash?
rehash操作,其实就是指扩大Hash表空间。而Redis实现rehash的基本思路是这样的:
* 首先,Redis准备了两个哈希表,用于rehash时交替保存数据。
我在前面给你介绍过,Redis在dict.h文件中使用dictht结构体定义了Hash表。不过,在实际使用Hash表时,Redis又在dict.h文件中,定义了一个dict结构体。这个结构体中有一个数组(ht\[2\]),包含了两个Hash表ht\[0\]和ht\[1\]。dict结构体的代码定义如下所示:
```
typedef struct dict {
…
dictht ht[2]; //两个Hash表,交替使用,用于rehash操作
long rehashidx; //Hash表是否在进行rehash的标识,-1表示没有进行rehash
…
} dict;
```
* 其次,在正常服务请求阶段,所有的键值对写入哈希表ht\[0\]。
* 接着,当进行rehash时,键值对被迁移到哈希表ht\[1\]中。
* 最后,当迁移完成后,ht\[0\]的空间会被释放,并把ht\[1\]的地址赋值给ht\[0\],ht\[1\]的表大小设置为0。这样一来,又回到了正常服务请求的阶段,ht\[0\]接收和服务请求,ht\[1\]作为下一次rehash时的迁移表。
这里我画了一张图,以便于你理解ht\[0\]和ht\[1\]交替使用的过程。
![](https://static001.geekbang.org/resource/image/1b/7f/1bc5b729yy127de43e0548ce0b6e6c7f.jpg?wh=2000x1125)
好,那么在了解了Redis交替使用两个Hash表实现rehash的基本思路后,我们还需要明确的是:在实现rehash时,都需要解决哪些问题?我认为主要有以下三点:
* 什么时候触发rehash?
* rehash扩容扩多大?
* rehash如何执行?
所以下面,我就带你来逐一学习Redis对这三个问题的代码实现,通过代码实现,你就能明晰Redis针对这三个问题的设计思想了。
### 什么时候触发rehash?
首先要知道,Redis用来判断是否触发rehash的函数是 **\_dictExpandIfNeeded**。所以接下来我们就先看看,\_dictExpandIfNeeded函数中进行扩容的触发条件;然后,我们再来了解下\_dictExpandIfNeeded又是在哪些函数中被调用的。
实际上,\_dictExpandIfNeeded函数中定义了三个扩容条件。
* 条件一:ht\[0\]的大小为0。
* 条件二:ht\[0\]承载的元素个数已经超过了ht\[0\]的大小,同时Hash表可以进行扩容。
* 条件三:ht\[0\]承载的元素个数,是ht\[0\]的大小的dict\_force\_resize\_ratio倍,其中,dict\_force\_resize\_ratio的默认值是5。
下面的代码就展示了\_dictExpandIfNeeded函数对这三个条件的定义,你可以看下。
```
//如果Hash表为空,将Hash表扩为初始大小
if (d->ht[0].size == 0)
return dictExpand(d, DICT_HT_INITIAL_SIZE);
//如果Hash表承载的元素个数超过其当前大小,并且可以进行扩容,或者Hash表承载的元素个数已是当前大小的5倍
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);
}
```
那么,对于条件一来说,此时Hash表是空的,所以Redis就需要将Hash表空间设置为初始大小,而这是初始化的工作,并不属于rehash操作。
而条件二和三就对应了rehash的场景。因为在这两个条件中,都比较了Hash表当前承载的元素个数(d->ht\[0\].used)和Hash表当前设定的大小(d->ht\[0\].size),这两个值的比值一般称为**负载因子(load factor)**。也就是说,Redis判断是否进行rehash的条件,就是看load factor是否大于等于1和是否大于5。
实际上,当load factor大于5时,就表明Hash表已经过载比较严重了,需要立刻进行库扩容。而当load factor大于等于1时,Redis还会再判断dict\_can\_resize这个变量值,查看当前是否可以进行扩容。
你可能要问了,这里的dict\_can\_resize变量值是啥呀?其实,这个变量值是在dictEnableResize和dictDisableResize两个函数中设置的,它们的作用分别是启用和禁止哈希表执行rehash功能,如下所示:
```
void dictEnableResize(void) {
dict_can_resize = 1;
}
void dictDisableResize(void) {
dict_can_resize = 0;
}
```
然后,这两个函数又被封装在了updateDictResizePolicy函数中。
updateDictResizePolicy函数是用来启用或禁用rehash扩容功能的,这个函数调用dictEnableResize函数启用扩容功能的条件是:**当前没有RDB子进程,并且也没有AOF子进程。**这就对应了Redis没有执行RDB快照和没有进行AOF重写的场景。你可以参考下面给出的代码:
```
void updateDictResizePolicy(void) {
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
dictEnableResize();
else
dictDisableResize();
}
```
好,到这里我们就了解了\_dictExpandIfNeeded对rehash的判断触发条件,那么现在,我们再来看下Redis会在哪些函数中,调用\_dictExpandIfNeeded进行判断。
首先,通过在[dict.c](https://github.com/redis/redis/blob/5.0/src/dict.c)文件中查看\_dictExpandIfNeeded的被调用关系,我们可以发现,\_dictExpandIfNeeded是被\_dictKeyIndex函数调用的,而\_dictKeyIndex函数又会被dictAddRaw函数调用,然后dictAddRaw会被以下三个函数调用。
* dictAdd:用来往Hash表中添加一个键值对。
* dictRelace:用来往Hash表中添加一个键值对,或者键值对存在时,修改键值对。
* dictAddorFind:直接调用dictAddRaw。
因此,当我们往Redis中写入新的键值对或是修改键值对时,Redis都会判断下是否需要进行rehash。这里你可以参考下面给出的示意图,其中就展示了\_dictExpandIfNeeded被调用的关系。
![](https://static001.geekbang.org/resource/image/90/11/90c261fce9dfe604e29239ba283cef11.jpg?wh=2000x969)
好了,简而言之,Redis中触发rehash操作的关键,就是\_dictExpandIfNeeded函数和updateDictResizePolicy函数。**\_dictExpandIfNeeded函数**会根据Hash表的负载因子以及能否进行rehash的标识,判断是否进行rehash,而**updateDictResizePolicy函数**会根据RDB和AOF的执行情况,启用或禁用rehash。
接下来,我们继续探讨Redis在实现rehash时,要解决的第二个问题:rehash扩容扩多大?
### rehash扩容扩多大?
在Redis中,rehash对Hash表空间的扩容是通过**调用dictExpand函数**来完成的。dictExpand函数的参数有两个,**一个是要扩容的Hash表,另一个是要扩到的容量**,下面的代码就展示了dictExpand函数的原型定义:
```
int dictExpand(dict *d, unsigned long size);
```
那么,对于一个Hash表来说,我们就可以根据前面提到的\_dictExpandIfNeeded函数,来判断是否要对其进行扩容。而一旦判断要扩容,Redis在执行rehash操作时,对Hash表扩容的思路也很简单,就是**如果当前表的已用空间大小为size,那么就将表扩容到size\*2的大小。**
如下所示,当\_dictExpandIfNeeded函数在判断了需要进行rehash后,就调用dictExpand进行扩容。这里你可以看到,rehash的扩容大小是当前ht\[0\]已使用大小的2倍。
```
dictExpand(d, d->ht[0].used*2);
```
而在dictExpand函数中,具体执行是由\_dictNextPower函数完成的,以下代码显示的Hash表扩容的操作,就是从Hash表的初始大小(DICT\_HT\_INITIAL\_SIZE),不停地乘以2,直到达到目标大小。
```
static unsigned long _dictNextPower(unsigned long size)
{
//哈希表的初始大小
unsigned long i = DICT_HT_INITIAL_SIZE;
//如果要扩容的大小已经超过最大值,则返回最大值加1
if (size >= LONG_MAX) return LONG_MAX + 1LU;
//扩容大小没有超过最大值
while(1) {
//如果扩容大小大于等于最大值,就返回截至当前扩到的大小
if (i >= size)
return i;
//每一步扩容都在现有大小基础上乘以2
i *= 2;
}
}
```
好,下面我们再来看看Redis要解决的第三个问题,即rehash要如何执行?而这个问题,本质上就是Redis要如何实现渐进式rehash设计。
### 渐进式rehash如何实现?
那么这里,我们要先搞清楚一个问题,就是**为什么要实现渐进式rehash?**
其实这是因为,Hash表在执行rehash时,由于Hash表空间扩大,原本映射到某一位置的键可能会被映射到一个新的位置上,因此,很多键就需要从原来的位置拷贝到新的位置。而在键拷贝时,由于Redis主线程无法执行其他请求,所以键拷贝会阻塞主线程,这样就会产生**rehash开销**。
而为了降低rehash开销,Redis就提出了渐进式rehash的方法。
简单来说,渐进式rehash的意思就是Redis并不会一次性把当前Hash表中的所有键,都拷贝到新位置,而是会分批拷贝,每次的键拷贝只拷贝Hash表中一个bucket中的哈希项。这样一来,每次键拷贝的时长有限,对主线程的影响也就有限了。
**那么,渐进式rehash在代码层面是如何实现的呢?**这里有两个关键函数:dictRehash和\_dictRehashStep。
我们先来看**dictRehash函数**,这个函数实际执行键拷贝,它的输入参数有两个,分别是全局哈希表(即前面提到的dict结构体,包含了ht\[0\]和ht\[1\])和需要进行键拷贝的桶数量(bucket数量)。
dictRehash函数的整体逻辑包括两部分:
* 首先,该函数会执行一个循环,根据要进行键拷贝的bucket数量n,依次完成这些bucket内部所有键的迁移。当然,如果ht\[0\]哈希表中的数据已经都迁移完成了,键拷贝的循环也会停止执行。
* 其次,在完成了n个bucket拷贝后,dictRehash函数的第二部分逻辑,就是判断ht\[0\]表中数据是否都已迁移完。如果都迁移完了,那么ht\[0\]的空间会被释放。因为Redis在处理请求时,代码逻辑中都是使用ht\[0\],所以当rehash执行完成后,虽然数据都在ht\[1\]中了,但Redis仍然会把ht\[1\]赋值给ht\[0\],以便其他部分的代码逻辑正常使用。
* 而在ht\[1\]赋值给ht\[0\]后,它的大小就会被重置为0,等待下一次rehash。与此同时,全局哈希表中的rehashidx变量会被标为-1,表示rehash结束了(这里的rehashidx变量用来表示rehash的进度,稍后我会给你具体解释)。
我画了下面这张图,展示了dictRehash的主要执行流程,你可以看下。
![](https://static001.geekbang.org/resource/image/e5/d1/e54b90dc143ba7e6eae2cda418ce20d1.jpg?wh=2000x1125)
同时,你也可以通过下面代码,来了解dictRehash函数的主要执行逻辑。
```
int dictRehash(dict *d, int n) {
int empty_visits = n*10;
...
//主循环,根据要拷贝的bucket数量n,循环n次后停止或ht[0]中的数据迁移完停止
while(n-- && d->ht[0].used != 0) {
...
}
//判断ht[0]的数据是否迁移完成
if (d->ht[0].used == 0) {
//ht[0]迁移完后,释放ht[0]内存空间
zfree(d->ht[0].table);
//让ht[0]指向ht[1],以便接受正常的请求
d->ht[0] = d->ht[1];
//重置ht[1]的大小为0
_dictReset(&d->ht[1]);
//设置全局哈希表的rehashidx标识为-1,表示rehash结束
d->rehashidx = -1;
//返回0,表示ht[0]中所有元素都迁移完
return 0;
}
//返回1,表示ht[0]中仍然有元素没有迁移完
return 1;
}
```
好,在了解了dictRehash函数的主体逻辑后,我们再看下渐进式rehash是如何按照bucket粒度拷贝数据的,这其实就和全局哈希表dict结构中的rehashidx变量相关了。
rehashidx变量表示的是**当前rehash在对哪个bucket做数据迁移**。比如,当rehashidx等于0时,表示对ht\[0\]中的第一个bucket进行数据迁移;当rehashidx等于1时,表示对ht\[0\]中的第二个bucket进行数据迁移,以此类推。
而dictRehash函数的主循环,首先会判断rehashidx指向的bucket是否为空,如果为空,那就将rehashidx的值加1,检查下一个bucket。
**那么,有没有可能连续几个bucket都为空呢?**其实是有可能的,在这种情况下,渐进式rehash不会一直递增rehashidx进行检查。这是因为一旦执行了rehash,Redis主线程就无法处理其他请求了。
所以,渐进式rehash在执行时设置了一个**变量empty\_visits**,用来表示已经检查过的空bucket,当检查了一定数量的空bucket后,这一轮的rehash就停止执行,转而继续处理外来请求,避免了对Redis性能的影响。下面的代码显示了这部分逻辑,你可以看下。
```
while(n-- && d->ht[0].used != 0) {
//如果当前要迁移的bucket中没有元素
while(d->ht[0].table[d->rehashidx] == NULL) {
//
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
...
}
```
而如果rehashidx指向的bucket有数据可以迁移,那么Redis就会把这个bucket中的哈希项依次取出来,并根据ht\[1\]的表空间大小,重新计算哈希项在ht\[1\]中的bucket位置,然后把这个哈希项赋值到ht\[1\]对应bucket中。
这样,每做完一个哈希项的迁移,ht\[0\]和ht\[1\]用来表示承载哈希项多少的变量used,就会分别减一和加一。当然,如果当前rehashidx指向的bucket中数据都迁移完了,rehashidx就会递增加1,指向下一个bucket。下面的代码显示了这一迁移过程。
```
while(n-- && d->ht[0].used != 0) {
...
//获得哈希表中哈希项
de = d->ht[0].table[d->rehashidx];
//如果rehashidx指向的bucket不为空
while(de) {
uint64_t h;
//获得同一个bucket中下一个哈希项
nextde = de->next;
//根据扩容后的哈希表ht[1]大小,计算当前哈希项在扩容后哈希表中的bucket位置
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
//将当前哈希项添加到扩容后的哈希表ht[1]中
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
//减少当前哈希表的哈希项个数
d->ht[0].used--;
//增加扩容后哈希表的哈希项个数
d->ht[1].used++;
//指向下一个哈希项
de = nextde;
}
//如果当前bucket中已经没有哈希项了,将该bucket置为NULL
d->ht[0].table[d->rehashidx] = NULL;
//将rehash加1,下一次将迁移下一个bucket中的元素
d->rehashidx++;
}
```
好了,到这里,我们就已经基本了解了dictRehash函数的全部逻辑。
现在我们知道,dictRehash函数本身是按照bucket粒度执行哈希项迁移的,它内部执行的bucket迁移个数,主要由传入的循环次数变量n来决定。但凡Redis要进行rehash操作,最终都会调用dictRehash函数。
接下来,我们来学习和渐进式rehash相关的第二个关键函数 **\_dictRehashStep**,这个函数实现了每次只对一个bucket执行rehash。
从Redis的源码中我们可以看到,一共会有5个函数通过调用\_dictRehashStep函数,进而调用dictRehash函数,来执行rehash,它们分别是:dictAddRaw,dictGenericDelete,dictFind,dictGetRandomKey,dictGetSomeKeys。
其中,dictAddRaw和dictGenericDelete函数,分别对应了往Redis中增加和删除键值对,而后三个函数则对应了在Redis中进行查询操作。下图展示了这些函数间的调用关系:
![](https://static001.geekbang.org/resource/image/05/0a/050cdce01b19a8d03834c18d1feab20a.jpg?wh=2000x820)
但你要注意,不管是增删查哪种操作,这5个函数调用的\_dictRehashStep函数,给dictRehash传入的循环次数变量n的值都为1,下面的代码就显示了这一传参的情况。
```
static void _dictRehashStep(dict *d) {
//给dictRehash传入的循环次数参数为1,表明每迁移完一个bucket ,就执行正常操作
if (d->iterators == 0) dictRehash(d,1);
}
```
这样一来,每次迁移完一个bucket,Hash表就会执行正常的增删查请求操作,这就是在代码层面实现渐进式rehash的方法。
## 小结
实现一个高性能的Hash表不仅是Redis的需求,也是很多计算机系统开发过程中的重要目标。而要想实现一个性能优异的Hash表,就需要重点解决哈希冲突和rehash开销这两个问题。
今天这节课,我带你学习了Redis中Hash表的结构设计、链式哈希方法的实现,以及渐进式rehash方法的设计实现。Redis中Hash表的结构设计很特别,它的每个哈希项都包含了一个指针,用于实现链式哈希。同时,Redis在全局哈希表中还包含了两个Hash表,这种设计思路也是为了在实现rehash时,帮助数据从一个表迁移到另一个表。
此外,Redis实现的渐进式rehash是一个用于Hash表扩容的通用方法,非常值得我们学习。这个设计方法的关键是每次仅迁移有限个数的bucket,避免一次性迁移给所有bucket带来的性能影响。当你掌握了渐进式rehash这个设计思想和实现方法,你就可以把它应用到自己的Hash表实现场景中。
## 每课一问
Hash函数会影响Hash表的查询效率及哈希冲突情况,那么,你能从Redis的源码中,找到Hash表使用的是哪一种Hash函数吗?
欢迎在留言区分享你的答案,如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。