redis dict字典的源码分析
2016-12-26 22:57
302 查看
Redis的字典用哈希表作为底层实现,一个哈希表里可以有很多哈希表节点,而每个哈希表节点就保存
了字典的一个键值对。
一、字典的基本实现
1、哈希表节点
2、哈希表
3、字典
为了让哈希表的负载因子维持在一个合理范围之内,当哈希表保存的键值对数量太多或者太少时,程序
需要对哈希表的大小进行扩展或者收缩。
调整哈希表的工作通过rehash(重新散列)来完成,rehash步骤如下:
1、为字典的ht[1]分配空间,空间大小取决于要执行的操作和ht[0]当前的包含的键值对数量(ht[0].used)
1)扩展操作,ht[1].size为第一个大于等于ht[0].used*2的2的n次方;
2)收缩操作,ht[1].size为第一个大于等于ht[0].used的2的n次方;
2、将保存在ht[0]中的键值对rehash到ht[1]上面:重新计算键的哈希值和索引值,然后将键值对放到ht[1]
哈希表的指定位置上。
3、当ht[0]的所有键值对都迁移到ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新建空白哈希表。
哈希表的收缩或者扩展
1、 当满足以下任一条件,程序将对哈希表进行扩张:
1)当服务器没有执行BGSAVE或者BGREWRITEAOF,并且负载因子大于1;
2)当服务器在执行BGSAVE或者BGREWRITEAOF,并且负载因子大于5;
2、当负载因子小于0.1时,程序将对哈希表进行收缩(主要为了节省空间)。
渐进式rehash
在redis中,rehash操作不是一次性完成的,而是分多次完成。这样做的目的主要是在redis中的dict可能会保存百万级、
千万级甚至亿级别个键值对,如果一次性将所有的键值对全部rehash到ht[1],巨大的的运算量将会导致redis在期间停止对外
服务。渐进式的rehash的实现关键是dict维持一个索引计数器rehashidx,并将它设为0,表示rehash开始。在rehash进行
期间,每次对字典执行添加、删除、查找或者更新操作时,程序会顺带从ht[0]上rehashidx索引开始遍历将第一个非空的哈
希桶的所有的键值对rehash到ht[1],rehash完成后,rehashidx加一。
在进行渐进式rehash过程中,dict同时使用ht[0]和ht[1]两个哈希表,所以字典的删除,查找,更新都会在两个哈希表上
进行。新添加的键值对都是一律保存到ht[1]里面,以保证ht[0]的键值对数量只减不增;其余操作都是先在ht[0]上查找,找
不到再到ht[1]查找进行操作。
.sizemask;
he = d->ht.table[idx];
……//遍历he,删除指定的键值对
}
return DICT_ERR; /* not found */
}
dictEntry *dictFind(dict *d, const void *key)
{
……//顺带迁移一个非空哈希桶上的所有键值对到ht[1]
if (dictIsRehashing(d)) _dictRehashStep(d);
……//先在ht[0]上查找,找不到再到ht[1]查找进行操作
for (table = 0; table <= 1; table++) {
idx = h & d->ht
了字典的一个键值对。
一、字典的基本实现
1、哈希表节点
typedef struct dictEntry { void *key;//键 union { void *val; uint64_t u64; int64_t s64; double d; } v;//值 struct dictEntry *next;//指向下一个节点,用于解决键冲突 } dictEntry;
2、哈希表
typedef struct dictht { dictEntry **table;//哈希表数组,每个元素都是指向dictEntry结构体指针 unsigned long size;//哈希表的大小 unsigned long sizemask;//哈希表大小掩码(等于size-1),用于计算索引值 unsigned long used;//哈希表已有节点数量 } dictht;
3、字典
typedef struct dict { dictType *type;//类型特定函数 void *privdata;//私有数据 dictht ht[2];//哈希表 long rehashidx; /* rehashing not in progress if rehashidx == -1 */ unsigned long iterators; /* number of iterators currently running */ } dict; 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;二、字典的基本操作
dict *dictCreate(dictType *type, void *privDataPtr);//创建新的字典 int dictAdd(dict *d, void *key, void *val);//将给定键值对添加到字典 int dictReplace(dict *d, void *key, void *val); //将给定键值对添加到字典,如果键已存在,则用新值替换旧值 void *dictFetchValue(dict *d, const void *key);//返回给定键的值 dictEntry *dictGetRandomKey(dict *d);//从字典中随机返回一个键值对 int dictDelete(dict *d, const void *key);//从字典中删除指定键值对 void dictRelease(dict *d);//释放给定字典及所有键值对三、rehash
为了让哈希表的负载因子维持在一个合理范围之内,当哈希表保存的键值对数量太多或者太少时,程序
需要对哈希表的大小进行扩展或者收缩。
调整哈希表的工作通过rehash(重新散列)来完成,rehash步骤如下:
1、为字典的ht[1]分配空间,空间大小取决于要执行的操作和ht[0]当前的包含的键值对数量(ht[0].used)
1)扩展操作,ht[1].size为第一个大于等于ht[0].used*2的2的n次方;
2)收缩操作,ht[1].size为第一个大于等于ht[0].used的2的n次方;
2、将保存在ht[0]中的键值对rehash到ht[1]上面:重新计算键的哈希值和索引值,然后将键值对放到ht[1]
哈希表的指定位置上。
3、当ht[0]的所有键值对都迁移到ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新建空白哈希表。
哈希表的收缩或者扩展
1、 当满足以下任一条件,程序将对哈希表进行扩张:
1)当服务器没有执行BGSAVE或者BGREWRITEAOF,并且负载因子大于1;
2)当服务器在执行BGSAVE或者BGREWRITEAOF,并且负载因子大于5;
//判断是否要扩展字典空间 static int _dictExpandIfNeeded(dict *d) { if (dictIsRehashing(d)) return DICT_OK; if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) {/*dict_can_resize为1表示服务器不在执行BGSAVE或者BGREWRITEAOF dict_can_resize为0表示服务器正在执行BGSAVE或者BGREWRITEAOF */ return dictExpand(d, d->ht[0].used*2); } return DICT_OK; }//给ht[1]分配第一个大于等于size的2的n次方大小的空间 int dictExpand(dict *d, unsigned long size) { dictht n; unsigned long realsize = _dictNextPower(size);//获取第一个大于等于size的2的n次方的值; …… n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; …… //为ht[1]分配空间,启动rehash d->ht[1] = n; d->rehashidx = 0; return DICT_OK; }
2、当负载因子小于0.1时,程序将对哈希表进行收缩(主要为了节省空间)。
//尝试收缩字典空间节省内存 void tryResizeHashTables(int dbid) { if (htNeedsResize(server.db[dbid].dict)) dictResize(server.db[dbid].dict); if (htNeedsResize(server.db[dbid].expires)) dictResize(server.db[dbid].expires); } //判断字典释放需要收缩空间 int htNeedsResize(dict *dict) { long long size, used; size = dictSlots(dict); used = dictSize(dict); //空间大于DICT_HT_INITIAL_SIZE,负载因子小于0.1 return (size > DICT_HT_INITIAL_SIZE && (used*100/size < HASHTABLE_MIN_FILL)); }//重设字典的空间 int dictResize(dict *d) { int minimal; if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR; minimal = d->ht[0].used; if (minimal < DICT_HT_INITIAL_SIZE) minimal = DICT_HT_INITIAL_SIZE; return dictExpand(d, minimal); }
渐进式rehash
在redis中,rehash操作不是一次性完成的,而是分多次完成。这样做的目的主要是在redis中的dict可能会保存百万级、
千万级甚至亿级别个键值对,如果一次性将所有的键值对全部rehash到ht[1],巨大的的运算量将会导致redis在期间停止对外
服务。渐进式的rehash的实现关键是dict维持一个索引计数器rehashidx,并将它设为0,表示rehash开始。在rehash进行
期间,每次对字典执行添加、删除、查找或者更新操作时,程序会顺带从ht[0]上rehashidx索引开始遍历将第一个非空的哈
希桶的所有的键值对rehash到ht[1],rehash完成后,rehashidx加一。
在进行渐进式rehash过程中,dict同时使用ht[0]和ht[1]两个哈希表,所以字典的删除,查找,更新都会在两个哈希表上
进行。新添加的键值对都是一律保存到ht[1]里面,以保证ht[0]的键值对数量只减不增;其余操作都是先在ht[0]上查找,找
不到再到ht[1]查找进行操作。
//将d->ht[0]中从索引d->rehashid开始的n个非空哈希桶的键值对迁移到ht[1] int dictRehash(dict *d, int n) { int empty_visits = n*10; if (!dictIsRehashing(d)) return 0; while(n-- && d->ht[0].used != 0) { dictEntry *de, *nextde; //从d->rehashidx开始遍历获取第一个非空哈希桶 assert(d->ht[0].size > (unsigned long)d->rehashidx); while(d->ht[0].table[d->rehashidx] == NULL) { d->rehashidx++;//遍历了n*10个空桶,就停止迁移 if (--empty_visits == 0) return 1; } de = d->ht[0].table[d->rehashidx]; //遍历哈希桶,将键值对重新哈希到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[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; } return 1; } static void _dictRehashStep(dict *d) { if (d->iterators == 0) dictRehash(d,1); }选几个函数,具体看了一下渐进式rehash的键值对迁移和期间字典操作实现
dictEntry *dictAddRaw(dict *d, void *key) { ……//顺带迁移一个非空哈希桶上的所有键值对到ht[1] if (dictIsRehashing(d)) _dictRehashStep(d); ……//如果在rehash,直接保存到ht[1] ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; entry = zmalloc(sizeof(*entry)); entry->next = ht->table[index]; ht->table[index] = entry; ht->used++; …… } static int dictGenericDelete(dict *d, const void *key, int nofree) { ……//顺带迁移一个非空哈希桶上的所有键值对到ht[1] if (dictIsRehashing(d)) _dictRehashStep(d); ……//先在ht[0]上查找,找不到再到ht[1]查找进行操作 for (table = 0; table <= 1; table++) { idx = h & d->ht