一.概述
Redis中使用字典这种数据结构作为数据库的底层实现,对数据库的增删改查也是基于对字典的操作之上进行的,当进行持久化时再将字典中的数据保存至磁盘中。
二.Redis中的字典结构
Redis中的字典是使用hashtable实现的,因为当hash表的负载因子不高且hash算法可以较均匀分配时,hashtable可以在常数时间内查找到数据,且若采用链式地址法处理冲突时,用头插法操作新来的元素,可以在O(1)时间复杂度内完成数据的插入操作。转而观之,rb_tree等却需要O(logn)的时间复杂度。
其字典结构定义如下:
// hash节点
typedef struct dictEntry{
// 键 (注意为void*, 因此可以指向任意类型)
void *key;
// 值(是一个共用体,可以保存指向任意类型的指针,或64位整型)
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// hash节点使用链式地址法处理冲突
struct dictEntry *next;
} dictEntry;
// hashtable结构体
typedef struct dictht {
// hashtable数组,table指向一个hashtable,其中每个元素为一个桶(指向hash节点的指针)
dictEntry **table;
// hashtable大小,即桶的个数
unsigned long size;
// hashtable大小掩码,为size - 1,用于计算索引(idx = hashIdx & sizemask)
unsigned long sizemask;
// hashtable中已有节点的数量
unsigned long used;
} dictht;
// 字典结构体
typedef struct dict {
// 字典操作的函数指针集合,可以根据用途不同的字典设置不同的函数(很类似操作系统中的写法)
dictType *type;
// 私用数据,用于作为上述函数的可选参数
// 参下方【注2】
void *privatedata;
// 两个hashtable,平时使用ht[0],在rehash时使用ht[1]进行辅助
dictht ht[2];
// 渐进式rehash时使用的索引,当未进行rehash时为-1
int trehashidx;
};
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;
【注1】:Redis的Hash表似乎借鉴了由华人内核维护者Herbert Xu为Linux内核开发的Hashtable, 在深入理解并行编程一书的第10章对这种Hashtable进行了说明。
【注2】:关于dict::privatedate的作用,在深入理解并行编程一书中给出了如下解释:用于扰乱hash函数,以防止通过对hash函数所使用的参数进行统计分析来发起拒绝服务攻击(DDos)。
三.hash算法
Redis采用MurmurHash2(不甚了解)算法,使用链地址法解决键冲突。
四.渐进式rehash
当要进行rehash,会先为ht[1]分配一个新的空间,接着采用渐进式rehash算法将ht[0]中的键-值对拷贝到ht[1]中,当拷贝完成后释放ht[0],将ht[1]赋给ht[0],ht[1]指向空哈希表(ht[1]->table = NULL)。
1.何时进行rehash
当以下两个条件之一被满足时会扩展hash表并进行rehash
- 当没有在执行BGSAVE(异步持久化)或BGREWRITEEAOF(异步重写一个AOF)命令时,哈希表的负载因子大于等于1。
- 当正在执行BGSAVE或BGBGREWRITEEAOF命令时,哈希表的负载因子大于等于5。
- 当负载因子小于0.1时进行哈希表的收缩
负载因子 = 哈希表已保存节点数量 / 哈希表大小 = ht[0].used / ht[0].size
【注】:之所以决定rehash的负载因子阈值随是否执行BGSAVE和BGREWRITEEAOF命令而变化是因为,在这两个命令的执行过程中,当前服务器进程都会fork出子进程来执行操作,而大多操作系统并不在fork出子进程时立即拷贝父进程的地址空间,而是采用写时赋值,而在rehash时要进行改变所以要进行写时赋值,为了减少写时赋值带来的性能影响而将负载因子提升到了5。(连写时赋值都考虑进去了,真的是无所不用其极)(关于写时赋值额具体过程,参考博文《Linux内核——进程的地址空间》)
2.hashtable的扩展与收缩规则
- 扩展:为ht[1]分配新的空间,大小为大于ht[0].used * 2(即现有哈希节点2倍)的第一个2^n。【注】:stl的hashtable是第一个质数
- 收缩:ht[1]指向的新哈希表空间为大于ht[0].used的第一个2^n。
【注】:哈希表的大小为2^n是有原因的,因为这样便于设置哈希表大小的掩码,即mask = size - 1,这样在计算元素放入哪个篮子时可以使用掩码快速计算,即哈希值 & mask
3.渐进式rehash过程
由于hash表可能比较庞大,因此若rehash操作一次性,集中性的完成会造成服务器一段时间内的停止服务,因此rehash操作是分多次,渐进式地完成更新的。(【开心】:和论文里想到的分批合并内存块的相法相同,嘻嘻~)。
rehash的详细步骤如下:
- 为ht[1]按规则分配一个新的哈希表,字典此时同时维护两个哈希表。
- 将字典中的trehashidx至为0,表示rehash操作开始(-1表示未进行rehash)
- 在rehash操作中除了对字典进行增删改查操作外还将ht[0]在rehashidx个桶上的键-值对rehash到ht[0]中。对于添加操作只在hash[0]中进行。【注】:不会在ht[0]与ht[1]中同时维护同一份数据,查找过程会先在ht[0]中查找,再去ht[1]中查找。
五.字典部分源码
1.字典初始化
调用dictCreate函数开辟字典空间,之后调用相应函数初始化内部数据成员,如下所示:
// 创建字典
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]); // 初始化用于键值对存储的hash表
_dictReset(&d->ht[1]);
d->type = type; // 初始化函数集合表
d->privdata = privDataPtr; // 初始化私有数据
d->rehashidx = -1; // 初始化rehash进度,时间事件函数serverCron会定期检查
d->iterators = 0; // 初始化当前迭代器数量为0
return DICT_OK;
}
// 初始化哈希表内部成员
static void _dictReset(dict *ht) {
ht->table = NULL;
ht->size = 0;
ht->sizemask = 0;
ht->used = 0;
}
2.收缩字典
将字典收缩至可满足存储数据元素的最小大小,但不小于DICT_HT_INITIAL_SIZE(4) , 亦用于初始化字典后调整哈希表大小。
// 收缩字典大小
int dictResize(dict *d)
{
int minimal;
// 若不允许收缩,或该字典当前正在进行rehash,则不可进行收缩
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used; // 该字典当前的元素大小
if (minimal < DICT_HT_INITIAL_SIZE) // 不可小于4
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal); // 调用dictExpand函数进行字典大小调整
}
int dictExpand(dict *d, unsigned long size)
{
dictht n; /* 新的哈希表 */
// _dictNextPower将哈希表调整至大于申请大小的第一个2的倍数大小(2^n)
unsigned long realsize = _dictNextPower(size);
//若当前字典正在进行rehash,或该字典的已有节点数大于要调整的字典大小(即不可容纳当前元素),则不进行调整
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
// 若调整后的大小与当前字典大小相同则不进行调整
if (realsize == d->ht[0].size) return DICT_ERR;
// 设置新哈希表成员
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0;
// 若当前字典中的哈希表0为空,说明当前处于初始化状态,则直接将新表赋予哈希表0
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
// 将新表赋予哈希表1,并进行rehash
d->ht[1] = n;
d->rehashidx = 0; // 启动rehash
return DICT_OK;
}
【注】:在事件事件处理函数serverCron中会调用databasesCron函数对服务器中的一部分数据库进行检查,在其中会检查相应字典是否需要进行rehash操作,而是否要进行rehash就是根据rehashidx是否为-1进行判断的,serverCron函数默认每100ms执行一次
3.rehash函数
// 对字典d进行rehash,此次至多更新n个非空篮子
int dictRehash(dict *d, int n) {
int empty_visits = n*10; // 最多可访问的空篮子个数,因为访问空篮子也是需要耗费一定时间的
if (!dictIsRehashing(d)) return 0; // 检查当前字典的rehash进度是否为-1,若是则说明不进行rehash
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* 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 long)d->rehashidx);
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx]; // 指向当前要进行rehash的篮子
// 将该篮子中的所有元素从旧表(ht[0])移入新表(ht[1])
while(de) {
unsigned int h;
nextde = de->next;
// 计算应该放入新表中的第几个篮子
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
// 清空旧表中的该篮子,并更新进度
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
// - 检查旧表中的元素是否已全部更新至新表中
// - 若是则将将ht[1]赋予ht[0],并重写初始化ht[1]
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
// 每次至多更新100个非空篮子,直至旧表更新完毕或超出限制时间则停止
int dictRehashMilliseconds(dict *d, int ms) {
long long start = timeInMilliseconds();
int rehashes = 0;
while(dictRehash(d,100)) {
rehashes += 100;
if (timeInMilliseconds()-start > ms) break;
}
return rehashes;
}
4.添加元素
将元素添加至字典
int dictAdd(dict *d, void *key, void *val)
{
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
// - 若当前无现存迭代器,则rehash一个篮子到新表中(加快rehash速度)
// - 若存在现存迭代器,则不进行rehash,因为会造成迭代器失效
if (dictIsRehashing(d)) _dictRehashStep(d);
// 检查字典中是否已存在该元素,若已存在则返回-1,不进行添加
if ((index = _dictKeyIndex(d, key)) == -1)
return NULL;
// - 根据是否正在进行rehash判断将新元素放入那个表中
// - 若正在进行rehash,则新增元素全部放入新表中,否则放入旧表
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
// 为新元素开辟节点空间,并链入相应篮子首部
entry = zmalloc(sizeof(*entry));
entry->next = ht->table[index];
ht->table[index] = entry;
ht->used++;
// dictSetKey函数调用字典中的函数集(type)中的函数设置该新增节点的值
dictSetKey(d, entry, key);
return entry;
}
5.替换元素
将键为key元素的值替换为新值,若该元素已存在,则由于值可能是用void*指向的一个对象,因此需先复制该节点,再更改该节点中的值,随后调用相应函数释放旧值
int dictReplace(dict *d, void *key, void *val)
{
dictEntry *entry, auxentry;
if (dictAdd(d, key, val) == DICT_OK)
return 1;
entry = dictFind(d, key);
// 复制旧节点(备份指向值的指针)
auxentry = *entry;
dictSetVal(d, entry, val);
// 调用函数集合表中的函数释放旧的节点(包括其中值指针所指向的元素)
dictFreeVal(d, &auxentry);
return 0;
}
6.删除元素
从字典中删除键值为key的元素
static int dictGenericDelete(dict *d, const void *key, int nofree)
{
unsigned int h, idx;
dictEntry *he, *prevHe;
int table;
if (d->ht[0].size == 0) return DICT_ERR; /* d->ht[0].table is NULL */
// - 若正在进行rehash
// - 若当前无现存迭代器,则rehash一个篮子到新表中(加快rehash速度)
// - 若存在现存迭代器,则不进行rehash,因为会造成迭代器失效
if (dictIsRehashing(d)) _dictRehashStep(d);
// 根据key调用哈希函数
h = dictHashKey(d, key);
// 若正在rehash则查找新旧量表,否则再查找ht[0]后结束
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
prevHe = NULL;
while(he) {
if (dictCompareKeys(d, key, he->key)) {
/* Unlink the element from the list */
if (prevHe)
prevHe->next = he->next;
else
d->ht[table].table[idx] = he->next;
if (!nofree) {
dictFreeKey(d, he);
dictFreeVal(d, he);
}
zfree(he);
d->ht[table].used--;
return DICT_OK;
}
prevHe = he;
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return DICT_ERR; /* not found */
}
int dictDelete(dict *ht, const void *key) {
return dictGenericDelete(ht,key,0);
}
7.字典遍历dictScan
为看明白,待更新!!!
8.随机返回数据节点(dictGetRandomKey)
dictEntry *dictGetRandomKey(dict *d)
{
dictEntry *he, *orighe;
unsigned int h;
int listlen, listele;
if (dictSize(d) == 0) return NULL;
if (dictIsRehashing(d)) _dictRehashStep(d);
if (dictIsRehashing(d)) { // 正在进行rehash
do {
// d->rehashidx + 保证了不会查询ht[0]中的0~d->rehashidx-1区间
h = d->rehashidx + (random() % (d->ht[0].size + d->ht[1].size - d->rehashidx));
he = (h >= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] :
d->ht[0].table[h];
} while(he == NULL);
} else {
do {
h = random() & d->ht[0].sizemask; // 随机一个篮子
he = d->ht[0].table[h]; // he指向该篮子的链表头
} while(he == NULL);
}
listlen = 0;
orighe = he;
while(he) { // 计算该篮子的长度
he = he->next;
listlen++;
}
// 随机取该篮子中的第几个数据节点
listele = random() % listlen;
he = orighe;
while(listele--) he = he->next;
return he;
}