字典, 又称为符号表 关联数组或者映射,是一种保存键值对的抽象数据结构.
字典做为一种经常使用数据结构被内置在许多程序语言中,因为 C 语言没有内置这种数据结构, Redis 构建了本身的字典实现.git
字典在 Redis 中的应用至关普遍, 好比 Redis 的数据库就是使用字典做为底层实现的, 对数据库的 增删改查操做也是构建在对字典的操做之上的.github
除了用做数据库以外, 字典仍是哈希键的底层之一, 当一个哈希键包含的键值对较多,欧哲键值对中的元素都是比较长的字符串时, Redis 就会使用字典做为哈希键的底层实现.算法
Redis 字典所使用的哈希表由 dict.h/dictht
结构定义:数据库
typedef struct dictht { dictEntry **table; //哈希表数组 unsigned long size; //哈希表大小 unsigned long sizemask; //用于计算索引值, //老是等于 size - 1 unsigned long used; //哈希表已有节点数量 } dictht;
table
属性是一个数组, 数组中每一个元素都是一个指向 dictEntry
结构的指针, 每一个 dictEntry
结构保存着一个键值对.编程
size
属性记录了哈希表的大小,也就是 table
数组的大小数组
sizemask
属性和哈希值一块儿决定一个键应该被放到 table
数组的哪一个索引上面服务器
哈希表节点使用 dictEntry
表示,每一个 dictEntry
结构保存着一个键值对数据结构
typedef struct dictEntry { void *key; //键 union { //值 void *val; uint_64 u64; int64_t s64; } v; sturct dictEntry *next; //指向下个哈希表节点,造成链表 } dictEntry;
注意这里 v 属性保存着键值对中的值,其中的键值能够是指针,或是 uint_64 整数,又或者是 int64_t 整数.ide
next
属性是指向另外一个哈希表节点的指针,这个指针将多个哈希值相同的键值对链接在一块儿,以此来解决键值冲突(collision)问题.函数
Redis 中的字典 由 dict.h/dict
结构表示:
typedef struct dict { dictType *type; //类型特定函数 void *privdata; //私有数据 dictht ht[2]; //哈希表 int rehashdx; //rehash 索引,当 rehash 不在进行时,值为-1 } dict;
type
和 privdata
是针对不一样类型的键值对,为建立多态字典而设置的
`type`指向一个 `dictType` 结构的指针, 每一个` dictType` 结构保存了一簇用于操做特做特定类型键值对的函数, Redis 会为用途不一样的字典设置不一样的类型特定函数. 而` pridata` 则保存了须要传给那些特定类型函数看可选参数.
ht
属性,包含两个数组,数组的每一项都是一个 dictht
哈希表,通常状况下字典只使用ht[0]
哈希表,ht[1]
哈希表只会在对哈希表进行 rehash 时使用.
rehashidex
记录了 rehash 当前的进度,若是没有进行 rehash, 值就为-1.
下图展现了一个普通状态下的字典(没有 rehash 进行)
当要将一个新的键值对添加到字典里面时,程序会根据键计算出哈希值和索引值,而后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上.
Redis 计算哈希值和索引的方法以下:
hash = dict->type->hashFunction(k); index = hash & dict->ht[0].sizemask
假设,要将上图中键值对 k1和v1添加到字典中,使用 hashFunction
计算出 k1 的哈希值为9,那么
index = 9 & 3 = 1;
Redis 使用 MurmurHash2 算法 来计算键的哈希值.
当有两个或两个以上的键被分配到了哈希表数组的同一索引上时,称这些键发生了冲突( collision
)
Redis 的哈希表使用链地址法来解决冲突,每一个哈希表节点都有一个 next
指针,多个哈希表节点能够用 next
指针构成一个单项链表,被分配到同一个索引上的多个节点能够用这个对单向链表链接起来,这就解决了键冲突的问题.
如前面的字典示意图所示, 键 k0 和 k1 的索引值均为1,这里只需用 next
指针将两个节点链接起来.
,由于dictEntry
节点组成的链表没有表尾指针,为了 速度考虑,程序老是将新节点调价到链表的表头位置,排在其余已有节点的前面,这样插入的复杂度为$ O(1)$.
随着操做的不断进行, 哈希表保存的键值对会逐渐地增多或减小,为了让哈希表的负载因子维持在一个合理的范围以内,当哈希表保存的键值对数量太多或者太少时, 程序须要对哈希表的大小进行相应的扩展或者收缩.这个过程叫作rehash
.
Redis 对字典的哈希表执行 rehash 的步骤以下:
为字典的 ht[1]
哈希表分配空间,空间的大小取决于要执行的操做,以及 ht0]
当前包含的键值对数量( used 属性值):
若是执行的是扩展操做,那么 ht[1]
的大小为第一个大于等于 ht0].used*2
的 $2^n$ .
若是执行的是收缩操做,那么ht[1]
的大小为打一个大于等于 ht[0].used
的$2^n$.
将保存在 ht[0]
中全部键值对 rehash
到 ht[1]
上面: 任何事指的是从新计算键的哈希值和索引值,而后键键值对放到 ht[1]
哈希表的指定位置.
当 ht[0]
包含的全部键值对都迁移到了 ht[1]
以后, 释放 ht[0]
, 再将 ht[1]
设置为 ht[0]
,并在 ht[1]
后面建立一个空白的哈希表.
举个例子,假设程序要对下图的 `ht[0] 进行扩展操做
ht[0].used 当前值为4 , $2^3$ 刚好是第一个大于等于 4*2 的值,因此 ht[1] 哈希表的大小设置为8,下图展现了 ht[1] 分配了空间以后字典的样子.
将 ht[0] 包含的四个键值对 rehash 到 ht[1], 图下图所示:
释放 ht[0], 将 ht[1] 设置为 ht[0]. 再分配一个空哈希表. 哈希表的大小由原来的4 扩展至8.
上一节说过, 扩展或收缩哈希表须要将 ht[0]
里的全部键值对 rehash 到 ht[1]
中,可是这个 rehash
动做并非一次性,集中式完成的,而是分屡次,渐进式完成的.
这么作的缘由是,当哈希表里保存的键值对多至百万甚至亿级别时,一次性地所有 rehash 的话,庞大的计算量会对服务器性能形成严重影响.
如下是渐进式 rehash 的步骤:
为 ht[1]
分配空间
在字典中维持一个索引计数器变量 rehashidx
, 将它的值设置为0,表示 rehash 正式开始
在 rehash 进行期间,每次对字典进行增删改查时,顺带将 ht[0] 在 rehashidx
索引上的全部键值对 rehash 到 ht[1] 中,同时将 rehashidx
加 1.
随着操做不断进行,最终在某个时间点上, ht[0]
全部的键值对所有 rehash 到 ht[1]
上,这是讲 rehashidx
属性置为 -1,,表示 rehash操做完成.
在渐进式 rehash 执行期间,新添加到字典的键值对一概保存到 ht[1]
里,不会对 ht[0]
作添加操做,这一措施保证了 ht[0]
只减不增,并随着 rehash 进行, 最终编程空表.
渐进式的 rehash 避免了集中式 rehash 带来 的庞大计算量和内存操做.
个人博客: http:ygmyth.github.io