【Redis】map实现原理

哈希表是一个很常用的数据结构,不同的平台对它有不同的实现。下面是redis的实现。

首先是redis中与哈希表有关的数据结构定义。

节点定义:

【Redis】map实现原理

这里有next指针的存在说明这是一个采用拉链法构建的哈希表。key是键,值是一个union,可以是以下的任意一种类型:指针,uint64和int64类型。

下面是table的实现:

【Redis】map实现原理

一个ht结构体就是哈希表的一个容器数组。table是一个指针的数组,每一个元素都指向了一个Entry节点。size是table的大小,sizemask是获取索引的掩码,userd是键值对的数目。

最后就是整个哈希表的实现:

【Redis】map实现原理

哈希表肯定需要一个table实例存放元素,但是这里是ht[2],表明有两个table,这与redis哈希表的扩容有关。包括最后的trehashids属性。还有一个type的指针,这个是与多态有关的,redis每一种数据类型都定义了一套方法,这些方法有:

【Redis】map实现原理

因为不同的类型的这些方法的实现时不同的,比如计算哈希值就不同,所以要为每一种类型定义乐淘方法。在java里面,因为是由对象类型或者clas存在的,所以可以很方便地使用多态来达到这个目的,但是在c语言里面,实现就比较困难。

在不进行rehash的正常情况下,哈希表里面只有ht[0]是有用的,ht[1]是空的。

所以哈希表的这样的:

【Redis】map实现原理

redis使用的是murmurHash哈希算法。

下面是redis rehash的实现。

大致过程如下:

(1)为ht[1]分配空间,分配空间的大小是第一个大于ht[0].used*2的2的n次方,所以说table的大小是2的n次方,至于为什么,只要是为了用掩码算索引是保证均匀性。

(2)把ht[0]里面的元素全部rehash到ht[1],释放ht[0]的空间,让ht[0]指向ht[1],再为ht[1]重新指向一个新的空的table。

其实看到这里,redis的实现与java的区别并没有特别大,只不过java里面没有设置两个table,而只是在resize函数里面直接新建一个局部的数组,rehash,再重新让table指向了新的数组,大致的过程一样。


但是,redis在做rehash的具体步骤与java不同。我们知道如果哈希表的元素非常多,那么做一次rehash会很耗时,java的hashmap就是一次性做完全部的rehash,肯定会有性能问题,而redis不是一次性rehash的,redis的做法叫做渐进式rehash,把每一个槽位的链表的rehash操作平摊到每一次对哈希表的操作上,其中dict结构中trehashidx就是指向了下一个需要rehash的槽位的索引,只要处于rehash的过程中,那么这个属性就是大于等于0的,如果没有处于rehash的过程,该值是-1。每rehash完一个槽位,那么该值会自增。在rehash的过程中,如果trehashidx所指向的槽位没有值,那么会去递归地寻找下一个trehashidx指向槽位不空的槽位。


【Redis】map实现原理

这就是redis哈希表实现上rehash的性能很高的原因。