3. 字典
3.1. 字典的概念
字典,又名符号表、关联数组或映射,是用来保存键值对的抽象数据结构。字典中的键唯一,且可以根据键对其与之关联的值进行改查删操作等,也可以根据键来删除整个键值对。
3.2. 字典的应用
- 作为Redis数据库的底层,对数据库增删改查是在字典的基础上进行的。
- 是哈希键底层的实现之一
- …还有很多
3.3. 字典的实现
3.3.1. 哈希表
字典是使用哈希表作为底层实现的。
哈希表的实现 dict.h/dictht
结构:
typedef struvt dictht{
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
}diictht;
属性介绍:
table
是一个数组,数组中的每个元素指向dict.h/dictEntry
结构的指针,每个dictEntry
结构都保存着一个键值对。size
记录着哈希表的大小,也就是table
数组的大小。used
表示哈希表已有的节点(键值对)的数量。sizemask
值永远等于size-1
,这个值和哈希值决定一个键应该放到table
数组的哪一个索引上。
3.3.2. 哈希表节点
dictEntry:
typedef struct dictEntry{
// 键
void *key;
// 值
union{
void *val;
uint64_t u64;
int64_t s64;
}v;
// 指向下一个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
属性解释:
key
保存键。v
保存值,这个值可以是uint64_t 整数,也可以是uint64_t整数,也可以是int64_t整数。- next属性指向另一个哈希表节点,可以将多个哈希值相同的键值对连接在一起,解决键冲突问题。
3.3.3. 字典
dict.h/dict结构如下:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdate;
// 哈希表
dictht ht[2];
// rehash索引
// 当rehash不在进行时,值为-1
int trehashidx;
}dict;
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;
属性解释:
type
是指向dictType
结构的指针,每个dictType
结构保存了一簇用于操作特定类型键值对的函数。privdata
属性保存需要传给那些特定函数的可选参数。ht
是一个包含两项的数组,每一个项都是dictht
哈希表,一般情况下只使用ht[0]
,ht[1]
只是在对ht[0]
哈希表进行rehash时使用。rehashidx
记录rehash的进度,如果没有正在进行rehash
,则值为-1。
3.4. 哈希算法
当我们要将新的键值对添加到字典里,这个时候需要先根绝键计算出哈希值和索引值,后根据索引值,将新键值对的哈希表节点放到哈希表数组的指定索引上。
3.4.1. Redis
计算哈希值和索引值
-
哈希值的计算
根据字典设置的哈希函数,计算
key
的哈希值hash = dict->type->hashFunction(key);
-
索引值的计算
使用哈希表的
sizemask
属性和哈希值,计算出索引值// x根据情况的不同,取0或1 index = hash & dict->ht[x].sizemask;
3.5. 解决键 冲突
当多个键都被分配到了哈希表数组的同一个索引上,就会产生键冲突。在Redis
哈希表中采用链地址法来解决键冲突,即每个哈希表节点都有一个next指针,多个哈希表节点可以用next
指针构成单向链表,被分配到同一个索引上的多个节点可以通过这个单向链表连接起来,从而解决键冲突。
3.6. rehash
哈希表的键值对的数量在不断变化,为了让哈希表的负载因子维持在合理的范围,当哈希表保存的键值对数量太多或者太少的时候,程序会对哈希表的大小进行拓展或者收缩,即通过rehash(重新散列)
操作来实现。
Redis
的执行rehash
步骤:
-
根据要执行的操作和
ht[0]
当前包含的键值对数量(ht[0].used
值)为ht[1]
(之前就介绍了ht[1]是为了实现rehash)分配合适的空间大小,- 如果执行的是扩展操作,那么
ht[1]
的大小为第一个大于等于ht[0].used*2
的2n。 - 如果执行的是收缩操作,那么
ht[1]
的大小为第一个大于等于ht[0]
.used的2n。
举个例子:如果used属性值为3,那么扩展操作,
ht[1]
的大小为8(23),收缩操作的话,ht[1]
的大小为4(22)。 - 如果执行的是扩展操作,那么
-
将保存在
ht[0]
中的键值对rehash到ht[1]
上,即重新计算键的哈希值和索引值,然后将键值对放置到ht[1]
的指定位置上。 -
当上面操作结束后,
ht[0]
为空表,释放ht[0]
,将ht[1]
设置为ht[0]
,并在ht[1]
新创建一个空白哈希表,为下一次rehash
做准备。
什么时候会进行哈希表的扩展和压缩?
- 当下列条件满足时,程序会自动对哈希表进行扩展操作。
服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因大于等于1。
服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
负载因子的计算公式
负载因子 = 哈希表已保存节点的数量 / 哈希表大小 load_factor = ht[0].used / ht[0].size 例如:对于一个大小为4,包含四个键值对的哈希表来说,负载因子的大小为 1
- 当满足哈希表的负载因子小于0.1时,程序会对哈希表进行收缩操作。
3.7 渐进式rehash
上面所说的扩展和收缩哈希表是分多次、渐进式完成的,这样才能保证在保存的键值对数量多的时候,不会因为计算量大,导致服务器停止服务。
渐进式rehash步骤:
- 为ht[1]分配空间,让字典同时拥有ht[0] 、ht[1]两个哈希表。
- 在字典中维持一个索引计数器变量rehashidx,并将其值设为0,表示rehash操作正在进行。
- 在rehash进行期间,每次对字典执行增删改查操作时,程序除了执行指定操作之外,还将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]上,当rehash工作完成,程序执行 rehashidx ++ 。
- 直到ht[0]所有键值对都被rehash到ht[1]上,将rehashidx值设为-1,表示rehash操作结束。
渐进式rehash的好处在于采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个增删改查操作上,避免计算量大导致服务器停止。
注:在执行渐进式rehash操作的时候,如果要查找到一个键的话,程序会先在ht[0]上查,如果咩查到,会查ht[1]。
3.8. API
函数 | 作用 | 时间复杂度 |
---|---|---|
dictCreate | 创建一个新的字典 | O(1) |
dictAdd | 将给定的键值对添加到字典上 | O(1) |
dictReplace | 将给定的键值对添加到字典里,如果已经存在,那么用新值替换旧值 | O(1) |
dictFetchValue | 返回给定键的值 | O(1) |
dictGetRandomKey | 从字典中随机返回一个键值对 | O(1) |
dictDelete | 从字典中删除给定键所对应的键值对 | O(1) |
dictRelease | 释放给定字典,以及字典中包含的所有键值对 | O(N),N为字典包含的键值对数量 |