字典dict
dict的主要实现从低到高依赖以下三个数据结构:
//具体的hash表的一项,用指正组成一个链表
typedef struct dictEntry {
void *key;//键
//值,联合体,可以使多种类型的值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;//hash冲突时使用开链发解决冲突,指向下一个节点
} dictEntry;
//dict结构体中的hash表的描述结构体
typedef struct dictht {
dictEntry **table;//指向dictEntry*的指针的数组
unsigned long size;//hash表的大小,即数组长度
unsigned long sizemask;//hash表大小的掩码,用于计算索引
unsigned long used;//hash表已有的节点数
} dictht;
//字典的主结构体
typedef struct dict {
dictType *type;
//如果这个字典有定义特殊的处理函数,则这个指针指向包含那些函数的指针的结构体
void *privdata;//私有数据
dictht ht[2];//hash表,当表长度不够rehash时需要用到第二张表
long rehashidx;
int iterators;
} dict;
redis里的字典在底层是用hash表来实现的,整个hash表的每一项的指针是一个地址连续的数组,要传入字典的参数根据相应的hash函数计算后存储在这个数组的某一项中,如果出现了hash冲突,则使用开链法解决,用struct dictEntry *next来组成一个链表,详细结构如下:
冲突解决与rehash
在使用开链法解决冲突问题的时候,链表的插入采用的是头插法(速度上的考虑)。
随着操作的不断进行,hash表保存的键值对一般会不断地增加,为了让表的负载因子维持在一个合理的范围之内,就需要用rehash对整个hash表进行调整。
rehash的步骤一般分为三步:
1. 为dict的ht[1]分配空间,空间一般为ht[0]的两倍
2. 通过重新计算hash的映射,把ht[0]里的项映射到ht[1]中
3. 映射完成后,释放ht[0]再把ht[1]赋给ht[0]。
由于rehash的整个过程会消耗大量时间,可能会导致整个redis在一段时间内停止服务,所以redis采取的是渐进式的rehash策略。在这种策略下,系统同时维持ht[0]和ht[1]两个hash表,然后在后台每隔n毫秒执行一次rehash的服务。rehash服务把ht[0]上的数据项rehash到ht[1]上,并做一下标识,直到rehash完毕。
rehash期间对dict的操作
由于rehash期间同时存在两个hash表,所以需要特别考虑rehash期间对hash表的操作。rehash期间,如果存在对hash表的更新、查找和删除操作,则需要同时在两个表中进行;如果存在对hash表的插入操作,则只在ht[1]中进行。