1、字典数据结构
1.1 哈希表数据结构
typedef struct dictht { //哈希表
dictEntry **table; //存放一个数组的地址,数组存放着哈希表节点dictEntry的地址。
unsigned long size; //哈希表table的大小
unsigned long sizemask; //用于将哈希值映射到table的位置索引。它的值总是等于(size-1)。
unsigned long used; //记录哈希表已有的节点(键值对)数量。
} dictht;
hash函数
可以将任意字符串转换成整型数,使其可以当作数组下标使用table
,是存放节点指针的数组。如dictEntry [sizemask]
,存放的是最后一个节点的地址,通过next
地址构成单链表解决哈希冲突
1.2 字典数据结构
typedef struct dict {
dictType *type; //指向dictType结构,dictType结构中包含自定义的函数,这些函数使得key和value能够存储任何类型的数据。
void *privdata; //私有数据,保存着dictType结构中函数的参数。
dictht ht[2]; //两张哈希表。
long rehashidx; //rehash的标记,rehashidx==-1,表示没在进行rehash
int iterators; //正在迭代的迭代器数量
} dict;
ht
包含两张哈希表,正常状态下,数据存储在ht[0]
中,当发生扩缩容时,使用ht[1]
过渡数据,扩缩容结束后,ht[1]
恢复到初始状态。rehashidx
默认值为-1,当发生rehash,即在扩缩容时,需要将ht[0]
数据迁移到ht[1]
数据时,rehashidx
值为索引值
2、字典扩缩容
2.1 扩缩容条件
- 已使用节点数和字典大小之间的比率超过
dict_force_resize_ratio
,默认值为5,则进行扩容 - 已使用节点数和字典大小之间的比率小于
HASHTABLE_MIN_FILL
,默认值为10(%),则进行缩容 - 扩容,
ht[1]
的大小为第一个大于等于ht[0].used* 2
的2^n; 缩容,ht[1]
的大小为第一个大于等于ht[0].used
的 2^n
2.2 扩容流程
- 申请一块新内存,初次申请时默认容量大小为4个
dictEntry
;非初次申请时,申请内存的大小则为当前Hash表容量的一倍。 - 把新申请的内存地址赋值给ht[1],并把字典的rehashidx标识由-1改为0,表示之后需要进行rehash操作
2.3 渐进式rehash
- 重新计算
ht[0]
中每个键的Hash值与索引值(重新计算就叫rehash),依次添加到新的Hash表ht[1],并把老Hash表中该键值对删除。把字典中字段rehashidx
字段修改为Hash表ht[0]中正在进行rehash
操作节点的索引值。 rehash
操作后,清空 ht[0],然后对调一下ht[1]与ht[0]的值,并把字典中rehashidx
字段标识为-1。
2.4 渐进式rehash场景
- 执行插入、删除、查找、修改等操作前,都先判断当前字典rehash操作是否在进行中,进行中则进行rehash操作(每次只对1个节点进行
rehash
操作,共执行1次) - 当服务空闲时,如果当前字典也需要进行
rehsh
操作,则会进行批量rehash
操作(每次对100个节点进行rehash操作,共执行1毫秒)。在经历N次rehash操作后,整个ht[0]的数据都会迁移到ht[1]中,这样做的好处就把是本应集中处理的时间分散到了上百万、千万、亿次操作中,所以其耗时可忽略不计。
3、迭代器
3.1 迭代器数据结构
迭代器主要用于遍历字典,遍历数据的原则为:不重复出现数据、不遗漏任何数据
typedef struct dictIterator {
dict *d; //被迭代的字典
long index; //迭代器当前所指向的哈希表索引位置
int table, safe; //table表示正迭代的哈希表号码,ht[0]或ht[1]。safe表示这个迭代器是否安全。
dictEntry *entry, *nextEntry; //entry指向当前迭代的哈希表节点,nextEntry则指向当前节点的下一个节点。
/* unsafe iterator fingerprint for misuse detection. */
long long fingerprint; //避免不安全迭代器的指纹标记
} dictIterator;
3.2 普通迭代器
- 迭代器初始化,
safe
字段为0 fingerprint
用来表示字典的指纹,通过对字典ht[0]、ht[1]两张哈希的地址、大小、已用大小进行指纹计算。在迭代器第一次迭代和释放时,会计算这两个状态时的指纹是否一致,不一致,会抛出异常- 普通迭代器,只遍历数据,不允许在遍历的过程中,发生字典的删除、新增操作
- 当发生
rehash
操作时,普通迭代器可能会出现重复数据
3.3 安全迭代器
- 迭代器初始化,
safe
字段为1 - 安全迭代器开始迭代时,字典的
iterators
值会+ 1,表示当前安全迭代器数目,当该值不为0时,不允许进行rehash
操作。
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1); //没有迭代器,进行1步rehash
}
4、discan间断遍历字典
4.1 索引更新方式
通常情况下,遍历情况下的索引更新方式为从0递增到size-1,可以保证所有数据不会遗漏。但在discan遍历方式中,索引更新方式采用了另外一种策略,从高位开始递增。代码如下,如长度为8的情况,索引顺序为000-100-010-110-001-101-011-111
。
v |= ~m1;//m1为哈希表掩码
v = rev(v);//二进制逆转
v++;
v = rev(v);//二进制逆转
static unsigned long rev(unsigned long v) { //翻转
unsigned long s = 8 * sizeof(v); // 位数bit size; must be power of 2
unsigned long mask = ~0; //s个位全为1
while ((s >>= 1) > 0) {
mask ^= (mask << s);
v = ((v >> s) & mask) | ((v << s) & ~mask);
}
return v;
}
4.2 遍历方式
针对遍历,我们要考虑三种情况,未进行扩缩容,迭代中正在进行rehash,迭代过程中已完成扩缩容。
- 未进行扩缩容,则只需按照当前索引更新方式遍历哈希表ht[0]即可。
- 迭代过程中已完成扩缩容,即仍只需要遍历哈希表ht[0]即可。实例如:假设hash表大小为4,进行第3次迭代时,hash表扩容到了8。此时我们发现,迭代只进行6次就完成了,顺序为0、2、1、5、3、7,扩容后少遍历了4、6,因为游标为0、2的数据在扩容前已经迭代完,而Hash表大小从4扩容至8,再经过rehash后,游标为0、2的数据可能会分布在0|4、2|6上,因此扩容后的游标4、6不需要再迭代。
- 迭代过程中正在rehash,则需要遍历哈希表ht[0]和ht[1],假设size更小的为t[0],大的为t[1],则根据当前的索引值,遍历t[0]上的数据,还需要遍历t[1]该索引值高位为1的数据。t[1]遍历的 条件为
while(v & (m0 ^ m1))
。实例如:ht[0]大小为8,ht[1]大小为32,当前索引为011,则在ht[1]需要遍历的位置顺序为00
011、10
011、01
011、11
011。高位逐渐+1的过程。