redis源码之字典dict
未完待续…
字典dict
1.简介:
它支持插入、删除、替换、查找和获取随机元素等操作。
哈希表会自动在表的大小的二次方之间进行调整。
键的冲突通过链表来解决。
rehash
2.定义
/*
* 1.哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
/*
* 2.哈希表
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
/*
* 3.字典
*/
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据,保存了需要传给那些类型特定画数的可选参数。
void *privdata;
// 哈希表,一般情况下,字典只使用ht[OJ 晴希表, ht[1]晗希表只会在对ht[0]哈希表进行rehash 时使用。
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
/*
* 4.字典类型特定函数
*/
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;
3.hash算法,得到索引值
Murmurhash2和DJBhash
Redis 计算晗希值和索引值的方法如下:
#使用字典设置的哈希函数,计算键key 的哈希值
hash= dict->type->hashFunction(key);
#使用哈希衰的sizemask 属性和哈希值,计算出索引值
#根据情况不同, ht[x]可以是ht[0l 或者ht[1]
index= hash 品dict->ht[x].sizemask;
解决键冲突:
链地址法(因为 dictEntry 节点组成的链表没有指向链表表尾的指针, 所以为了速度考虑,程序总是将新节点添加到链表的表头位置(复杂度为O(1)), 排在其他已有节点的前面。 )
4.rehash
为了让晗希表的负载因子( load factor )维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对晗希表的大小进行相应的扩展或者收缩。
(1)为字典的ht[1]哈希表分配空间:
(1)如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0] .used*2
的2”( 2 的n 次方幕);
(2)如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0] .used的2”;、
(2)将保存在ht[0]中的所有键值对rehash 到ht[1]上面: rehash 指的是重新计算键的晗希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
(3)当ht[0]包含的所有键值对都迁移到了ht[1]之后( ht [0]变为空表),释放
ht[0],将ht [1]设置为ht[0] ,并在ht[1]新创建一个空白哈希表,为下一次rehash
做准备。
哈希表的扩展与收缩条件
负载因子 = 哈希表已保存节点数量 / 哈希表大小,load factor= ht[O].used I ht[OJ.size
负载因子大于1是因为,size是table的数量,而每个table还有链表呢 !
当以下条件中的任意一个被满足时,程序会自动开始对晗希表执行扩展操作:
(1)服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于于 1 ;
(2)服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;
当晗希表的负载因子小于0.1 时,程序自动开始对晗希表执行收缩操作。
在执行BGSA阳命令或BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制( copy-onwrite)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行晗希表扩展操作,这可以避免不必要的内存写入操作,最大限度地节约内存。
5.渐进式 rehash:索引0->sizemask
rehash 动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。原因在于,一次性将这些键值对全部rehash到ht[1]的话,庞大的计算量可能会导致服务器在一段时间内停止服务。
以下是哈希表渐进式 rehash 的详细步骤:
(1)为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
(2)在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
(3)在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增1。
(4)随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
1.因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。
2.另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。
typedef struct dictIterator {
// 被迭代的字典
dict *d;
// table :正在被迭代的哈希表号码,值可以是 0 或 1 。
// index :迭代器当前所指向的哈希表索引位置。
// safe :标识这个迭代器是否安全
int table, index, safe;
// entry :当前迭代到的节点的指针
// nextEntry :当前迭代节点的下一个节点
// 因为在安全迭代器运作时, entry 所指向的节点可能会被修改,
// 所以需要一个额外的指针来保存下一节点的位置,
// 从而防止指针丢失
dictEntry *entry, *nextEntry;
long long fingerprint; // unsafe iterator fingerprint for misuse detection
} dictIterator;