Redis基本数据结构-字典
1、字典概念
Redis是一个键值对数据库,在很多地方用到字典。Redis字典的实现采用的是比较经典的哈希表方式实现的。貌似跟memcached的方法有点像,很久之前看过部分memcached,现在忘得差不多了。Redis的字典定义如下:
/*
* 字典
*
* 每个字典使用两个哈希表,用于实现渐进式 rehash
*/
typedef struct dict {
// 特定于类型的处理函数
dictType *type;
// 类型处理函数的私有数据
void *privdata;
// 哈希表(2 个)
dictht ht[2];
// 记录 rehash 进度的标志,值为 -1 表示 rehash 未进行
int rehashidx;
// 当前正在运作的安全迭代器数量
int iterators;
} dict;
/*
* 哈希表
*/
typedef struct dictht {
// 哈希表节点指针数组(俗称桶,bucket)
dictEntry **table;
// 指针数组的大小
unsigned long size;
// 指针数组的长度掩码,用于计算索引值
unsigned long sizemask;
// 哈希表现有的节点数量
unsigned long used;
} dictht;
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 链往后继节点
struct dictEntry *next;
} dictEntry;
使用2个哈希表,这是为了后面要说到的扩容准备的。每个哈希表如果有冲突,则采用链表方式解决。哈希表节点的指针数组为table字段,注意这是个两级指针,我习惯这样看dictEntry *table[],即table数组的每个值都是一个指向dictEntry结构体的指针。其他三个字段就是指针数组大小、长度掩码以及当前节点数。比如长度为4的话,掩码就是3。哈希表节点则是一个简单的单项链表,存储有键、值以及后继节点的指针。当不同的键哈希后的值发生冲突时,采用链表连接。
2、相关函数
2.1)创建字典
dict *dictCreate(dictType *type,
void *privDataPtr)
{
// 分配空间
dict *d = zmalloc(sizeof(*d));
// 初始化字典
_dictInit(d,type,privDataPtr);
return d;
}
/*
* 初始化字典
*
* T = O(1)
*/
int _dictInit(dict *d, dictType *type,
void *privDataPtr)
{
// 初始化哈希表ht[0]
_dictReset(&d->ht[0]);
// 初始化哈希表ht[1]
_dictReset(&d->ht[1]);
// 初始化字典属性
d->type = type;
d->privdata = privDataPtr;
d->rehashidx = -1; //rehash标志,初始设置-1表示没有rehash
d->iterators = 0;
return DICT_OK;
}
/*
* 重置哈希表的各项属性
*
* T = O(1)
*/
static void _dictReset(dictht *ht)
{
ht->table = NULL;
ht->size = 0;
ht->sizemask = 0;
ht->used = 0;
}
初始化过程需要注意的是type
变量的设置,
dictType
结构体类型的
type
是涉及到字典存储对象的一系列的操作方法,有点多态的味道,不同的对象赋值的
type
不同。
2.2)添加键值对到字典
/*
* 添加给定 key-value 对到字典
*
* T = O(1)
*/
int dictAdd(dict *d, void *key, void *val)
{
// 添加 key 到哈希表,返回包含该 key 的节点
dictEntry *entry = dictAddRaw(d,key);
// 添加失败?
if (!entry) return DICT_ERR;
// 设置节点的值
dictSetVal(d, entry, val);
return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key)
{
int index;
dictEntry *entry;
dictht *ht;
// 尝试渐进式地 rehash 一个元素
if (dictIsRehashing(d)) _dictRehashStep(d);
// 查找可容纳新元素的索引位置
// 如果元素已存在, index 为 -1
if ((index = _dictKeyIndex(d, key)) == -1)
return NULL;
/* Allocate the memory and store the new entry */
// 决定该把新元素放在那个哈希表
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
// 为新元素分配节点空间
entry = zmalloc(sizeof(*entry));
// 新节点的后继指针指向旧的表头节点
entry->next = ht->table[index];
// 设置新节点为表头
ht->table[index] = entry;
// 更新已有节点数量
ht->used++;
/* Set the hash entry fields. */
// 关联起节点和 key
dictSetKey(d, entry, key);
// 返回新节点
return entry;
}
添加键值对到字典主要的执行方法在dictAddRaw函数。首先会判断字典当前是否正在rehash,如果是,则调用_dictRehashStep(d)方法rehash哈希表中table数组的一个桶中元素。Redis的字典采用的是渐进式rehash方法,即在扩展哈希表的时候不会一次性将所有的元素都从老的哈希表移重新哈希到新的哈希表中,因为这样会使得本次操作延迟过长。
rehash标识是字典中的rehashidx变量,初始为-1表示没有rehash,而随着rehash的进行,这个值会设定为当前哈希到的table项。在每次扩容哈希表时,就会设置rehashidx标识。而从dictAddRaw方法看到,在新增键值对到字典时,首先要调用dictKeyIndex(d, key)方法根据key计算新增键值对的索引,即应该是table中的第几项,然后根据是否在rehash将该元素添加到正确的哈希表中,并更新字典的节点数目。最后设置key对应的value是在dictSetVal(d, entry, val)中完成。
在根据key计算索引的方法dictKeyIndex(d, key)中,首先会判断哈希表是否需要扩容:1)如果是第一次添加元素,则需要创建一个新的哈希表,大小设置为4,并设置rehashidx变量为0。 2)如果使用的节点数大于table的大小而且dict_can_resize 为真(或者已用节点数除以哈希表大小之比大于dict_force_resize_ratio),则也需要扩容哈希表为当前使用节点数的2倍大小。(注:如果哈希表本身在rehash过程中,扩容函数会直接返回,不会执行扩容)。然后查找字典的两个哈希表查找key(如果哈希表此时没有rehash,则只需要查找ht[0].table对应的项即可),如果已经存在,则返回-1,否则返回key在table中的索引值。
渐进式rehash是在扩容后下次dictAdd的时候开始执行,每次移动table中的一个项的元素到新的哈希表中。如第一次移动ht[0].table[0],第二次ht[0].table[1]中的所有元素,其实每一个项都是一个链表。如果有安全迭代器,则是不能够rehash 的。渐进式rehash跟memcached很类似的说。
/*
* 执行 N 步渐进式 rehash 。
*
* 如果执行之后哈希表还有元素需要 rehash ,那么返回 1 。
* 如果哈希表里面所有元素已经迁移完毕,那么返回 0 。
*
* 每步 rehash 都会移动哈希表数组内某个索引上的整个链表节点,
* 所以从 ht[0] 迁移到 ht[1] 的 key 可能不止一个。
*
* T = O(N)
*/
int dictRehash(dict *d, int n) {
if (!dictIsRehashing(d)) return 0;
while(n--) {
dictEntry *de, *nextde;
// 如果 ht[0] 已经为空,那么迁移完毕
// 用 ht[1] 代替原来的 ht[0]
if (d->ht[0].used == 0) {
// 释放 ht[0] 的哈希表数组
zfree(d->ht[0].table);
// 将 ht[0] 指向 ht[1]
d->ht[0] = d->ht[1];
// 清空 ht[1] 的指针
_dictReset(&d->ht[1]);
// 关闭 rehash 标识
d->rehashidx = -1;
// 通知调用者, rehash 完毕
return 0;
}
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(d->ht[0].size > (unsigned)d->rehashidx);
// 移动到数组中首个不为 NULL 链表的索引上
while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;
// 指向链表头
de = d->ht[0].table[d->rehashidx];
// 将链表内的所有元素从 ht[0] 迁移到 ht[1]
// 因为桶内的元素通常只有一个,或者不多于某个特定比率
// 所以可以将这个操作看作 O(1)
while(de) {
unsigned int h;
nextde = de->next;
/* Get the index in the new hash table */
// 计算元素在 ht[1] 的哈希值
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
// 添加节点到 ht[1] ,调整指针
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
// 更新计数器
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
// 设置指针为 NULL ,方便下次 rehash 时跳过
d->ht[0].table[d->rehashidx] = NULL;
// 前进至下一索引
d->rehashidx++;
}
// 通知调用者,还有元素等待 rehash
return 1;
}
3)获取元素值
/*
* 返回在字典中, key 所对应的值 value
*
* 如果 key 不存在于字典,那么返回 NULL
*
* T = O(1)
*/
void *dictFetchValue(dict *d, const void *key) {
dictEntry *he;
he = dictFind(d,key);
return he ? dictGetVal(he) : NULL;
}
首先调用dictFind函数查找字典key是否存在,存在则调用dictGetVal函数获取值。判断key是否存在是通过dictCompareKeys函数判定,而该函数在dictType结构体中赋值。如果是rehash状态,则需要查找两个哈希表并返回key对应的dictEntry项。
dictGetVal函数则是返回dictEntry项的val项即可。
3、其他
字典还有其他的函数,暂时就不分析代码了,后面有用到再细看。
4、参考资料
http://www.redisbook.com/en/latest/internal-datastruct/dict.html