- 字典,又称映射(map),是一种保存键值对的抽象数据结构。这种数据结构是内置在很多高级语言中,但是Redis所使用的C语言并没有内置这种数据结构,因为Redis构建了自己的字典实现。
- Redis数据库就是使用字典来作为底层实现的,对数据库的增。删、差、改操作也是构建在对字典的操作之上的。
一、字典的实现
Redis的字典是使用哈希表作为底层实现的,一个哈希表里可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
1、哈希表
typedef struct dictht {
dictEntry **table
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
结构解释:
- table : table是一个数组,数组中每个元素都是指向dictEntry结构的指针,每个dictEntry结构保存着一个键值对;
- size : 哈希表的大小,即table数组的大小;
- sizemask :其值总是等于size-1。sizemask和哈希值一起决定一个键应该被放到table数组的哪个索引上面;
- used :哈希表目前已有节点(键值对)的数量;
eg:下图为一个空的哈希表

哈希表节点的结构定义
哈希表节点用dictEntry结构表示,每个dictEntry结构都保存着一个键值对:
typedef struct dictEntry {
void *key;
union{
void *val;
uint64_tu64;
int64_ts64;
} v;
struct dictEntry *next;
} dictEntry;
注:其中next是指向下一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,用于解决哈希冲突的问题。

2、字典
了解完哈希表的结构后,我们来了解哈希表是怎么实现字典这个结构的。
字典结构:
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
int trehashidx;
} dict;
type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的。
- type:一个指向dictType结构的指针,每个dictType用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
- privdata:保存了需要传给给那些类型特定函数的可选参数。
- ht:是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表, 一般情况下,字典只使用ht[0] 哈希表, ht[1]哈希表只会对ht[0]哈希表进行rehash时使用。
- rehashidx:记录了rehash目前的进度,如果目前没有进行rehash,值为-1。
读到这里,相信我们都大概清楚Redis字典结构组成是怎么样的,以下是参考图:

3、字典中是如何解决哈希冲突的——链地址法
当有两个或以上的键由于哈希值相同被分配到哈希表数组中的同一个索引位置上,便发生了哈希冲突,为了解决这种冲突,我们是在数组的每个索引位置上拉出一条链表,将被分配到这个索引上的键值用链表连接起来。(注意:新节点会插到链表的头部)
二、字典中哈希表的扩展/收缩策略——渐进式rehash
随着操作的不断执行,哈希表保存的键值会逐渐的增多或减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太大或者太小时,程序需要对哈希表的大小进行相应的扩容或收缩。即定期进行rehash以提高性能或者节约内存。
1、哈希表rehash的步骤
- 为字典的ht[1]散列表分配空间,这个空间的大小取决于要执行的操作以及ht[0]当前包含的键值对数量(即:ht[0].used的属性值):
- 扩展时:ht[1]的大小等于第一个大于等于ht[0].used*2的2的n次方幂。如eg:ht[0].used=3时ht[1]的大小为8;ht[0].used=4时ht[1]的大小为8。
- 收缩时:ht[1]的大小等于第一个大于等于ht[0].used的2的n次方幂。如eg:ht[0].used=3时ht[1]的大小为4;ht[0].used=6时ht[1]的大小为8;
- 将保存在ht[0]中所有的键值对rehash到ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
- 当ht[0]包含的所有键值对都迁移到ht[1]之后,ht[0]则变为空表了,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。
2、程序何时对哈希表执行rehash?
当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:
- 服务器目前没有执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
- 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
- 而当负载因子小于0.1时,程序自动开始对哈希表执行收缩操作
哈希表负载因子公式:load_factor = ht[0].used / ht[0].size
3、渐进式rehash
上面说到,扩展或收缩哈希表需要将ht[0]里面的所有键值对rehash到ht[1]里面,但是这个rehash动作不是一次性的、集中式地完成,而是分多次、渐进式的完成的。因为一次性的将大量的键值对rehash到ht[1],会很影响服务器的性能。
渐进式rehash的步骤:
- 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
- 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 ,表示 rehash 工作正式开始。
- 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成后, 程序将 rehashidx 属性的值增1。
- 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
注意:
- .因为在进行渐进式 rehash 的过程中,字典会同时使用 ht[0] 和 ht[1] 两个哈希表,所以在渐进式 rehash 进行期间,字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行。
- 在渐进式 rehash 执行期间,新添加到字典的键值对一律会被保存到 ht[1] 里面,而 ht[0] 则不再进行任何添加操作:这一措施保证了 ht[0] 包含的键值对数量会只减不增,并随着 rehash 操作的执行而最终变成空表。