1. 介绍
字典又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。例如:redis中的所有key到value的映射,就是通过字典结构维护,还有hash类型的键值。
通过redis中的命令感受一下哈希键。
user键在底层实现就是一个字典,字典包含3个键值对。
2. 字典的实现
redis的字典是由哈希表实现的,一个哈希表有多个节点,每个节点保存一个键值对。
2.1 哈希表
redis中哈希表定义在dict.h/dictht中。
2.2 哈希表节点
哈希表的table指向的数组存放这dictEntry类型的地址。也定义在dict.h/dictEntryt中。
下图展现的就是通过链接法(chaining)来解决冲突的方法。
[url01]
2.3 字典
字典结构定义在dict.h/dict中。
dictType类型保存着 操作字典不同类型key和value的方法 的指针。
下图展现的就是刚才命令user哈希键所展现的内部结构:
[url02]
3. 哈希算法
Thomas Wang认为好的hash函数具有两个好的特点:
- hash函数是可逆的。
- 具有雪崩效应,意思是,输入值1bit位的变化会造成输出值1/2的bit位发生变化
3.1 计算int整型哈希值的哈希函数
|
|
3.2 MurmurHash2哈希算法
当字典被用作数据库的底层实现,或者哈希键的底层实现时,redis用MurmurHash2算法来计算哈希值,能产生32-bit或64-bit哈希值。
3.3 djb哈希算法
djb哈希算法,算法的思想是利用字符串中的ascii码值与一个随机seed,通过len次变换,得到最后的hash值。
4. rehash
当哈希表的大小不能满足需求,就可能会有两个或者以上数量的键被分配到了哈希表数组上的同一个索引上,于是就发生冲突(collision),在Redis中解决冲突的办法是链接法(separate chaining)。但是需要尽可能避免冲突,希望哈希表的负载因子(load factor),维持在一个合理的范围之内,就需要对哈希表进行扩展或收缩。
Redis对哈希表的rehash操作步骤如下:
- 扩展或收缩
- 扩展:ht[1]的大小为第一个大于等于ht[0].used * 2的 2n 。
- 收缩:ht[1]的大小为第一个大于等于ht[0].used的 2n 。
- 将所有的ht[0]上的节点rehash到ht[1]上。
- 释放ht[0],将ht[1]设置为第0号表,并创建新的ht[1]。
源码:
扩展操作
12345678910111213141516171819202122static int _dictExpandIfNeeded(dict *d) //扩展d字典,并初始化{/* Incremental rehashing already in progress. Return. */if (dictIsRehashing(d)) return DICT_OK; //正在进行rehash,直接返回/* If the hash table is empty expand it to the initial size. */if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); //如果字典(的 0 号哈希表)为空,那么创建并返回初始化大小的 0 号哈希表/* If we reached the 1:1 ratio, and we are allowed to resize the hash* table (global setting) or we should avoid it but the ratio between* elements/buckets is over the "safe" threshold, we resize doubling* the number of buckets. *///1. 字典已使用节点数和字典大小之间的比率接近 1:1//2. 能够扩展的标志为真//3. 已使用节点数和字典大小之间的比率超过 dict_force_resize_ratioif (d->ht[0].used >= d->ht[0].size && (dict_can_resize ||d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)){return dictExpand(d, d->ht[0].used*2); //扩展为节点个数的2倍}return DICT_OK;}收缩操作:
123456789101112int dictResize(dict *d) //缩小字典d{int minimal;//如果dict_can_resize被设置成0,表示不能进行rehash,或正在进行rehash,返回出错标志DICT_ERRif (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;minimal = d->ht[0].used; //获得已经有的节点数量作为最小限度minimalif (minimal < DICT_HT_INITIAL_SIZE)//但是minimal不能小于最低值DICT_HT_INITIAL_SIZE(4)minimal = DICT_HT_INITIAL_SIZE;return dictExpand(d, minimal); //用minimal调整字典d的大小}
扩展和收缩操作都调用了dictExpand()函数,该函数通过计算传入的第二个大小参数进行计算,算出一个最接近2n的realsize,然后进行扩展或收缩,dictExpand()函数源码如下:
收缩或者扩展哈希表需要将ht[0]表中的所有键全部rehash到ht[1]中,但是rehash操作不是一次性、集中式完成的,而是分多次,渐进式,断续进行的,这样才不会对服务器性能造成影响。因此下面介绍渐进式rehash。
5. 渐进式rehash(incremental rehashing)
渐进式rehash的关键:
- 字典结构dict中的一个成员rehashidx,当rehashidx为-1时表示不进行rehash,当rehashidx值为0时,表示开始进行rehash。
- 在rehash期间,每次对字典的添加、删除、查找、或更新操作时,都会判断是否正在进行rehash操作,如果是,则顺带进行单步rehash,并将rehashidx+1。
- 当rehash时进行完成时,将rehashidx置为-1,表示完成rehash。
源码在此:
6. 迭代器
redis在字典结构也定义了迭代器
迭代器分为安全迭代器和不安全迭代器:
非安全迭代器只能进行Get等读的操作, 而安全迭代器则可以进行iterator支持的任何操作。
由于dict结构中保存了safe iterators的数量,如果数量不为0, 是不能进行下一步的rehash的; 因此安全迭代器的存在保证了遍历数据的准确性。
在非安全迭代器的迭代过程中, 会通过fingerprint方法来校验iterator在初始化与释放时字典的hash值是否一致; 如果不一致说明迭代过程中发生了非法操作.