字典
又称关联数组(associative array),映射(map),符号表(symbol table)
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对。
哈希表节点
dict.h/dictEntry
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
unit64_t u64;
int 64_t s64;
}v;
// 指向下一个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
哈希表
dict.h/dictht
typedef struct dictht {
// 哈希表节点数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值,总是等于size-1
unsigned long sizemask;
// 哈希表节点数量(以被使用的数量)
unsigned long used;
}dictht;
字典类型
typedef struct dictType {
// 计算哈希值
unsigned int (*hashFunction)(const void *key);
// 复制键
void *(*keyDup)(void *privdata, const void *key);
// 复制值
void *(*valDup)*void *privdata, const void *obj);
// 对比键
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键
void (*keyDestructor)(void *privdata, void *key);
// 销毁值
void (*valDestructor)(void *privdata, void *obj);
}dictType;
字典
dict.h/dict
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有函数
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引,如果目前没有进行rehash,值为-1
int rehashidx;
}dict;
哈希算法
当要将一个新的键值对添加到字典时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。
Redis计算哈希值和索引值:
// 使用字段类型特定函数,计算键key的哈希值
hash = dict->type->hashFunction(key);
// 使用哈希表的sizemask属性值和上面的哈希值(hash),计算处索引
// 根据不同的情况,ht[x]可以是ht[0]或者ht[1]
index = hash & dict->ht[x].sizemask;
解决键冲突
定义:
当有两个或者以上数量的键被分配到了哈希表数组的同一个索引上面时,称之为键发生了冲突。
例如:
程序要将键值对 k2 -> v2 添加到哈希表里面,并且计算出 k2 的索引值 为 2,然而索引 2 已经保存了一个键值对 k1 -> v1,这时 k1 和 k2 将产生冲突。
方法:
Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以使用next指针构成一个单向链表,被分配到同一个索引上的多个节点使用单向链表连接起来,从而解决键冲突问题。
特点:
出于速度上的考虑,程序总是将新节点添加的链表的表头位置(复杂度为O(1)),排在其他已有节点的前面。
渐进式 rehash
随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。
满足以下任一个条件,程序将自动开启对哈希表执行扩展操作:
- 服务器目前没有在执行BGSAVE命名或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
- 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于5。
哈希表负载因子计算方式
// 负载因子 = 哈希表已使用节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size;
当哈希表的负载因子小于0.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期间哈希表操作
读取(find)、更新(update)、删除(delete):
同时在两个链表上进行,如果要查找一个键,程序会现在ht[0]找,如果没有则去ht[1]查找。
新增:
新增的键值对一律保存到ht[1]里面,而ht[0]不再进行任何新增操作,从而保证ht[0]包含的键值对只减不增