一、 dict的定义
??字典,是一种用于实现键值对(key-value pair)保存的抽象数据结构,通过字典,可以在单个键(key)与单个值(value)之间进行关联(或者说是将键映射成值),而这些关联的键与值即为键值对。
??在字典中,每一个键都是独一无二的,所以程序可以在字典中通过键来对值,甚至是键值对进行操作。
??在一些高级编程语言中,字典经常作为一种内置的数据结构出现,但是可惜的是,C语言并不在此列,所以 Redis 自己实现了字典这种数据结构。
??在 Redis 中,字典使用哈希表作为底层实现,而一个哈希表可以有多个哈希结点,每一个哈希节点保存了一组键值对。下图是一个典型的字典:
??下面依次是哈希节点、哈希表、字典、迭代器的实现。
哈希节点(dictEntry )
哈希节点,在 dict.h/dictEntry 中进行了定义:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next;
} dictEntry;
-
key 属性用来保存键值对中的键,联合体(union)v 属性用来保存键值对中的值。由于 v 属性使用 union 关键字进行定义,所以键值对中的值可以是一个指针,一个 uint64_t 类型的整数,或者是一个 int64_t 类型的整数。 -
* next 属性是指向下一个哈希节点的指针,用来将多个哈希值相同的键值对连接起来,构成一个单向链表,用以解决键冲突(collision)即通过链地址法解决键冲突。
哈希表(dictht )
哈希表,在 dict.h/dictht 中进行了定义:
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
-
table 属性是一个数组,数组中的每一个元素都是一个指向 dict.h/dictEntry 结构的指针。 -
size 属性记录了哈希表的大小,也即是 table 数组的大小。 -
used 属性记录了哈希表已有结点,即键值对的数量。 -
sizemask 属性的值总是等于 size-1 ,是用来和哈希值一起决定一个键应该被放在 table 数组的哪一个索引上面的。
字典(dict)
字典,在 dict.h/dict 中进行了定义:
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
int rehashidx;
int iterators;
} dict;
-
ht属性:每一个字典(dict)都有两个哈希表(dictht),用来实现 渐进式 rehash ,保存在 ht 数组当中。并且,在字典当中,一般只会使用 ht[0] 哈希表,只有在对 ht[0] 哈希表进行 rehash 时才会用到 ht[1]。 -
rehashidx 属性用来记录 rehash 进度,当 rehash 没有在进行的时候,rehashidx = -1 。 -
iterators 属性用来记录目前正在运行的迭代器的数量。 -
type 属性和 privdata 属性是用来针对不同类型的键值对,创建多态字典的。
- type 属性是一个指向 dictType 结构的指针,每个 dictType 结构保存了一组用于操纵特定类型键值对的函数,而 Redis 会为不同用途的字典设置不同的类型特定函数。
- privdata 属性保存了要传给那些类型特定函数的可选参数。
其中,字典类型特定函数在 dict.h/dictType 中进行了定义:
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;
extern dictType dictTypeHeapStringCopyKey;
extern dictType dictTypeHeapStrings;
extern dictType dictTypeHeapStringCopyKeyValue;
字典迭代器(dictIterator )
字典迭代器,在 dict.h/dictIterator 中进行了定义:
typedef struct dictIterator {
dict *d;
int table, index, safe;
dictEntry *entry, *nextEntry;
long long fingerprint;
} dictIterator;
-
d 属性是一个指向 dict 结构的指针,保存了当前迭代器正在处理的字典。 -
table 属性用来标识当前正在被迭代的哈希表,即 dict 结构中的 dictht 数组 ht 的下标,值可以为 0 或 1 。 -
index 属性用来标识迭代器当前所指向的哈希表的索引位置。 -
safe 属性用来标识这个迭代器是否是安全的迭代器,这对该迭代器后续能调用的 API 有影响。其中,safe = 1 表示该迭代器可以在迭代时进行增删改等操作,对字典进行修改,即调用 dictDelete、dictFind 等函数,如若不然,则只能使用 dictNext 遍历函数,而不能对字典进行修改。 -
entry 属性是一个指向 dictEntry 结构的指针,指向当前迭代到的哈希节点。 -
nextEntry 属性是一个指向 dictEntry 结构的指针,指向当前迭代到的哈希节点的下一个哈希节点。这个属性存在的原因是,在安全迭代器运行时,entry 指针所指向的节点可能会被修改,所以需要一个指针来保存下一节点的位置,来防止指针丢失。 -
fingerprint 属性是字典的指纹,用于误用检测,即在不安全的迭代器对字典进行操作后,与操作前的指纹值进行对比,若不同,则说明在迭代过程中有对字典进行增删操作,而这些操作在不安全的迭代器中是不被允许的。
宏定义常量
在 dict.h 中,对一些对常量的定义:
#define DICT_OK 0
#define DICT_ERR 1
#define DICT_HT_INITIAL_SIZE 4
即:
二、 dict 的 API
dict 的函数实现分为两种,一种是通过宏定义的函数,一种是在 dict.c 内实现的函数。
1. 宏定义函数
函数 | 作用 | 时间复杂度 |
---|
DICT_NOTUSED | 用于在字典的私有数据不使用时,让编译器忽略该私有数据,避免编译器错误 | O(1) | dictFreeVal | 释放字典中给定哈希节点的值 | O(1) | dictSetVal | 设置字典中给定哈希节点的值 | O(1) | dictSetSignedIntegerVal | 将一个有符号整数设置为给定哈希节点的值 | O(1) | dictSetUnsignedIntegerVal | 将一个无符号整数设置为给定哈希节点的值 | O(1) | dictFreeKey | 释放字典中给定哈希节点的键 | O(1) | dictSetKey | 设置字典中给定哈希节点的键 | O(1) | dictCompareKeys | 比较两个键 | O(1) | dictHashKey | 计算给定字典中给定键的哈希值 | O(1) | dictGetKey | 获取给定哈希节点的键 | O(1) | dictGetVal | 获取给定哈希节点的值 | O(1) | dictGetSignedIntegerVal | 获取给定哈希节点的有符号整数 | O(1) | dictGetUnsignedIntegerVal | 获取给定哈希节点的无符号整数 | O(1) | dictSlots | 获取给定字典的大小,即 ht[0].size + ht[1].size | O(1) | dictSize | 获取给定字典的已有节点数量,即 ht[0].used+ ht[1].used | O(1) | dictIsRehashing | 判断指定字典是否正在进行 rehash | O(1) |
2. dict.c 中实现的函数
在 dict.c 中定义了几个静态变量:
static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;
static uint32_t dict_hash_function_seed = 5381;
-
dict_can_resize ??表示字典是否启用 rehash ,程序可以通过后续在API 实现提及的dictEnableResize() 和 dictDisableResize() 函数,来手动地允许或阻止哈希表进行 rehash。但是,即使该值为零,也不是所有的 rehash 都会被阻止,这种情况与下列的 dict_force_resize_ratio 变量有关。 -
dict_force_resize_ratio ??字典强制 rehash 比率。当某个哈希表的已有哈希节点的数量和字典大小之间的比率大于字典强制 rehash 比率,即 size / used > dict_force_resize_ratio 时,即使 dict_can_resize = 0 也会强制执行 rehash。 -
dict_hash_function_seed ??哈希种子,用于哈希函数之一的 dictGenCaseHashFunction() 函数中。
而在 dict.c 中实现的函数,有以下几种类型:
(1)私有函数
函数 | 作用 | 时间复杂度 |
---|
_dictExpandIfNeeded | 根据需要,初始化字典的哈希表,或者对字典的现有哈希表进行扩展。 函数内部是通过调用 dictExpand() 函数来实现的 | O(N) | _dictNextPower | 对字典的哈希表 ht[0] 的大小做幂级扩展,即计算第一个大于等于 ht[0].size 的 2^N ,用作 rehash 操作需要的 ht[1] 的大小 | O(1) | _dictKeyIndex | 返回哈希表中可以将给定的 key 插入的索引位置。 如果哈希表正在进行 rehash ,那么返回的索引总是新的哈希表的索引,即 ht[1] 的索引,因为在 rehash 进行的过程中,插入的新节点总是插入到 ht[1] 当中的 | O(N) | _dictInit | 初始化字典 | O(1) |
(2)哈希函数
函数 | 作用 | 备注 |
---|
dictIdentityHashFunction | 直接使用 key 作为哈希值 | 无 | dictIntHashFunction | 对一个整数进行哈希 | Thomas Wang’s 32 bit Mix Function | dictGenHashFunction | 对字符串进行哈希 | MurmurHash2, by Austin Appleby | dictGenCaseHashFunction | 对大小写不敏感的字符串进行哈希 | 基于 DJB 哈希 | dictSetHashFunctionSeed | 设置哈希种子 | 即 dict_hash_function_seed | dictGetHashFunctionSeed | 获取哈希种子 | 即 dict_hash_function_seed |
具体实现与分析日后单开一篇进行学习与分析。
(3)其余 API 实现
函数 | 作用 | 时间复杂度 |
---|
_dictReset | 对给定哈希表的各项属性值进行重置或初始化 | O(1) | dictCreate | 创建一个新的字典,内部调用了 _dictInit() 函数用以实现字典的初始化 | O(1) | dictResize | 缩小给定的字典的负载因子,即 ht[0].used / ht[0].size,让其比例接近于 1 : 1。 内部通过调用dictExpand() 函数实现 | O(N) | dictExpand | 扩展或创建一个新的哈希表 具体来说,先创建一个新的哈希表,之后: 1. 如果给定的字典的 ht[0] 不存在,那么将新的哈希表设置为 ht[0] ,即意味着创建一个新的哈希表 2. 如果给定的字典的 ht[0] 存在,那么将新的哈希表设置为 ht[1] ,并且将字典的 rehashidx 属性设置为 0,使程序可以开始对字典进行 rehash,即对已有哈希表进行扩展 | O(N) | dictRehash | 执行 N 步渐进式 rehash,将给定字典的哈希表 ht[0] 当中的哈希节点分 N 步 rehash 到哈希表 ht[1] 当中。 返回 0,表示所有键都已经从 ht[0] 迁移到了 ht[1] 当中。 返回 1,表示仍有键需要从 ht[0] 迁移到 ht[1] 当中。 需要注意的是,每步 rehash 都是以一个哈希表索引(桶)作为单位的,即每一步 rehash 之后,被 rehash 的桶里的所有节点都会被移动到新哈希表 | O(N) | timeInMilliseconds | 获取以毫秒为单位的 UNIX 时间戳 | O(1) | dictRehashMilliseconds | 在给定毫秒数内,以 100 步为单位,对字典进行 rehash (调用 dictRehash() 函数),返回该函数调用期间的 rehash 步数 | O(N) | _dictRehashStep | 在字典不存在安全迭代器的情况下,对字典进行单步 rehash。字典在有安全迭代器的情况下,是不能进行 rehash 的。 这个函数被多个查找、更新操作调用,使得字典在被使用的同时进行 rehash | O(1) | dictAdd | 尝试将给定键值对添加到字典中。 内部调用了 dictAddRaw() 函数,通过对调用该函数返回的哈希节点的 val 属性赋值完成操作 | O(N) | dictAddRaw | 尝试将键插入到字典中,是比 dictAdd() 函数更加底层的函数。 如果键在字典中已经存在,返回 NULL;否则,程序会创建新的哈希结点,插入到字典相应的索引位置,为该哈希节点的 key 属性赋值,并返回该哈希节点,交由用户为该哈希节点的 val 属性赋值(即在 dictAdd() 函数中对该哈希节点的 val 属性赋值) | O(N) | dictReplace | 将给定的键值对添加到字典中,如果键已经存在,那么对旧有的哈希节点的值进行更新。 如果键值对是全新添加的,返回 1;如果键值对是通过对原有的键值对更新得来的,返回 0 | O(N) | dictReplaceRaw | 查找函数,根据给定的 key 查找哈希节点并返回。 如果给定的 key 已经存在,返回包含该 key 的哈希节点;如果给定的 key 不存在,那么调用 dictAddRaw() 函数将 key 添加到字典当中,再返回包含该 key 的哈希节点。 总之,该函数总是会返回包含给定 key 的哈希节点 | O(N) | dictGenericDelete | 从字典中删除包含给定键的哈希节点,通过函数的 nofree 参数可以选择是否调用键和值的释放函数来删除键值 | O(1) | dictDelete | 从字典中删除包含给定键的哈希节点,并且调用键值的释放函数来删除键值 | O(1) | dictDeleteNoFree | 从字典中删除包含给定键的哈希节点,并且不调用键值的释放函数来删除键值 | O(1) | _dictClear | 删除哈希表上的所有节点,并重置哈希表的各项属性 | O(N) | dictRelease | 删除并释放整个字典 | O(N) | dictFind | 获取字典中包含键 key 的哈希节点 | O(1) | dictFetchValue | 获取包含给定键 key 的哈希节点的值 | O(1) | dictFingerprint | 获取给定字典的指纹。 指纹是一个 64 位的数字,用于表示字典在某个时间的状态。指纹在创建和释放一个不安全迭代器时获取,并且两次获取的指纹会进行比对,如果指纹不一致,那么说明在迭代过程中执行了被禁止的操作 | O(1) | dictGetIterator | 创建并返回给定字典的不安全迭代器 | O(1) | dictGetSafeIterator | 创建并返回给定字典的安全迭代器 | O(1) | dictNext | 获取迭代器指向的当前节点 | O(1) | dictReleaseIterator | 释放给定字典迭代器 | O(1) | dictGetRandomKey | 随机返回字典中任意一个节点 | O(N) | dictGetRandomKeys | 随机返回字典中多个节点 | O(N) | rev | 反转位,工具函数 | O(1) | dictScan | 对字典中的哈希节点进行迭代。 具体源码与算法日后分析 | O(N) | dictEmpty | 清空字典中的两个哈希表上的所有哈希节点,并重置字典属性 | O(N) | dictEnableResize | 开启自动 rehash,即将 dict_can_resize 设为 1 | O(1) | dictDisableResize | 关闭自动 rehash,即将 dict_can_resize 设为 0 | O(1) |
三、 dict 的特性
-
每个字典带有两个哈希表 ht[0] 与 ht[1] ??由于随着操作的进行,哈希表中保存的键值对数量会随之不断地变化,为了使哈希表的负载因子保持在一个合理的范围,会对哈希表进行相应的扩展或者收缩,所以需要进行 rehash 操作。 ??所以每个字典都会带有两个哈希表 ht[0] 与 ht[1],其中,ht[0] 用于在平时使用,而 ht[1] 用于在 rehash 进行时使用:将 ht[0] 上的所有结点 rehash 到 ht[1] 上。 ??rehash,指的是对键的哈希值与索引值重新计算,然后将键值对放到 ht[1] 的指定位置。 ??在 rehash 进行时,新添加的哈希节点会被添加到哈希表 ht[1] 当中,确保哈希表 ht[0] 当中的结点只减不增。当 ht[0] 表中的结点全部迁移到 ht[1] 表中后,会释放 ht[0] 表,将 ht[1] 设置为 ht[0],并重置 ht[1]。 -
渐进式 Rehash ??对于上述提到的 rehash 操作,并不是一次性全部迁移完成,而是分多次、渐进式地完成的,因为在实际环境中,哈希表中保存的节点实际上是非常多的,如果一次性全部迁移的话,会对服务器的性能造成很大影响,甚至会导致服务器服务停止。 ??具体来说,就是在对字典进行添加、删除、查找和更新操作时,会在需要 rehash 操作时进行单步 rehash,从而将庞大的工作量分摊开来。 -
键冲突 ??哈希表使用链地址法来解决键冲突,即被分配到同一索引的多个键值会拉成一个单向链表,正因为如此,在查找操作时难免会用到循环结构,而新的哈希节点插入时也是采用头插法来实现的。
|