Redis 中,字典是基础结构。Redis 数据库数据、过时时间、哈希类型都是把字典做为底层结构。html
哈希表的实现代码在:dict.h/dictht
,Redis 的字典用哈希表的方式实现。redis
typedef struct dictht { // 哈希表数组,俗称的哈希桶(bucket) dictEntry **table; // 哈希表的长度 unsigned long size; // 哈希表的长度掩码,用来计算索引值,保证不越界。老是 size - 1 // h = dictHashKey(ht, he->key) & n.sizemask; unsigned long sizemask; // 哈希表已经使用的节点数 unsigned long used; } dictht;
table
是一个哈希表数组,每一个节点的实如今 dict.h/dictEntry
,每一个 dictEntry
保存一个键值对。size
属性记录了向系统申请的哈希表的长度,不必定都用完,有预留空间的。sizemask
属性主要是用来计算 索引值 = 哈希值 & sizemask
,这个索引值决定了键值对放在 table
的哪一个位置。它的值老是 size - 1
,其实我有点不明白为啥计算的时候不直接用 size - 1
,知道的大佬请明示。used
属性用来记录已经使用的节点数,size
- use
就是未使用的节点啦。下图展现了一个大小为 4 的空哈希表结构,没有任何键值对
算法
哈希表 dictht
的 table
的元素由哈希节点 dictEntry
组成,每个 dictEntry
就是一个键值对数据库
typedef struct dictEntry { // 键 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; double d; } v; // 下一个哈希节点,用于哈希冲突时拉链表用的 struct dictEntry *next; } dictEntry;
next 指针是用于当哈希冲突的时候,能够造成链表用的。后续会将数组
Redis 的字典实如今: dict.h/dict
。函数
typedef struct dict { // 哈希算法 dictType *type; // 私有数据,用于不一样类型的哈希算法的参数 void *privdata; // 两个哈希表,用两个的缘由是 rehash 扩容缩容用的 dictht ht[2]; // rehash 进行到的索引值,当没有在 rehash 的时候,为 -1 long rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 正在跑的迭代器 unsigned long iterators; /* number of iterators currently running */ } dict; // dictType 实际上就是哈希算法,不知道为啥名字叫 dictType typedef struct dictType { // hash方法,根据 key 计算哈希值 uint64_t (*hashFunction)(const void *key); // 复制 key void *(*keyDup)(void *privdata, const void *key); // 复制 value void *(*valDup)(void *privdata, const void *obj); // key 比较 int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 销毁 key void (*keyDestructor)(void *privdata, void *key); // 销毁 value void (*valDestructor)(void *privdata, void *obj); } dictType;
dictType
属性表示字典类型,实际上这个字典类型就是一组操做键值对算法,里面规定了不少函数。
privdata
则是为不一样类型的 dictType
提供的可选参数。
若是有须要,在建立字典的时候,能够传入dictType
和 privdata
。性能
dict.cui
// 建立字典,这里有 type 和 privdata 能够传 dict *dictCreate(dictType *type, void *privDataPtr) { dict *d = zmalloc(sizeof(*d)); _dictInit(d,type,privDataPtr); return d; } // 初始化字典 int _dictInit(dict *d, dictType *type, void *privDataPtr) { _dictReset(&d->ht[0]); _dictReset(&d->ht[1]); d->type = type; d->privdata = privDataPtr; d->rehashidx = -1; d->iterators = 0; return DICT_OK; }
下图是比较完整的普通状态下的 dict
的结构(没有进行 rehash,也没有迭代器的状态):
# 哈希算法
当字典中须要添加新的键值对时,须要先对键进行哈希,算出哈希值,而后在根据字典的长度,算出索引值。spa
// 使用哈希字典里面的哈希算法,算出哈希值 hash = dict->type->hashFunction(key) // 使用 sizemask 和 哈希值算出索引值 idx = hash & d->ht[table].sizemask; // 经过索引值,定位哈希节点 he = d->ht[table].table[idx];
哈希冲突指的是多个不一样的 key,算出的索引值同样。设计
Redis 解决哈希冲突的方法是:拉链法。就是每一个哈希节点后面有个 next
指针,当发现计算出的索引值对应的位置有其余节点,那么直接加在前面节点后便可,这样就造成了一个链表。
下图展现了 {k1, v1}
和 {k2, v2}
哈希冲突的结构。
假设 k1
和 k2
算出的索引值都是 3,当 k2
发现 table[3]
已经有 dictEntry{k1,v1}
,那就 dictEntry{k1,v1}.next = dictEntry{k2,v2}
。
随着操做的不断进行,哈希表的长度会不断增减。哈希表的长度太长会形成空间浪费,过短哈希冲突明显致使性能降低,哈希表须要经过扩容或缩容,让哈希表的长度保持在一个合理的范围内。
Redis 经过 ht[0] 和 ht[1] 来完成 rehash 的操做,步骤以下:
ht[0].used * 2
的 \(2^n\) 的数,例如 ht[0].used = 3,那么分配的是距离 6 最近的 \(2^3=8\)ht[0].used / 2
的 \(2^n\) 的数,例如 ht[0].used = 6,那么分配的是距离 3 最近的 \(2^2=4\)h[0] = h[1]
,并把 h[1] 清空,为下次 rehash 准备上面说的 rehash 中的第二步,迁移的过程不是一次完成的。若是哈希表的长度比较小,一次完成很快。可是若是哈希表很长,例如百万千万,那这个迁移的过程就没有那么快了,会形成命令阻塞!
下面来讲说,redis 是如何渐进式地将 h[0]
中的键值对迁移到 h[1]
中的:
rehashidx
维护了 rehash 的进度,设置为 0 的时候,开始 rehashrehashidx
上的整条链表迁移到 h[1]
中。迁移完以后 rehashidx + 1
h[0]
上的全部键值对都会迁移到 h[1]
中。所有迁移完成以后 rehashidx = -1
这种渐进式 rehash 的方式的好处在于,将庞大的迁移工做,分摊到每次的增删改查中,避免了一次性操做带来的性能的巨大损耗。
缺点就是迁移过程当中 h[0]
和 h[1]
同时存在的时间比较长,空间利用率较低。
下面一系列的图,演示了字典是如何渐进式地 rehash ( 图片来自 《Redis 设计与实现》图片集 )