Redis使用字典的方式实现了数据库键空间,今天就记录一下字典的实现方式。
Redis 对字典的描述和实现源码在 src/dict.h src/dict.c,关于学习Redis字典如何进行测试或debug,请参考另外一篇文章:Redis 2.8.9源码 - 字典哈希表操作函数头整理,并注释作用和参数说明(附测试方法和代码以及使用方法)
字典结构:
typedef struct dict { dictType *type; //根据存储内容的不同,自定义一组回调函数来控制比较申请内存释放空间等操作 void *privdata; //用于回调函数使用 dictht ht[2]; //存放hash表的结构 0 默认使用0,rehash的时候使用1 int rehashidx; //默认为-1,rehash开始的时候该变量设置为0,完成后重新设置为-1 int iterators; //正在进行的安全迭代的数量,释放安全迭代后会减1 } dict;
typedef struct dictType { unsigned int (*hashFunction)(const void *key); //针对不同类型进行hash计算的函数,返回hash值 void *(*keyDup)(void *privdata, const void *key); //复制key void *(*valDup)(void *privdata, const void *obj); //复制val int (*keyCompare)(void *privdata, const void *key1, const void *key2); //比较key void (*keyDestructor)(void *privdata, void *key); //释放key void (*valDestructor)(void *privdata, void *obj); //释放val } dictType;
哈希表结构:
typedef struct dictht { //hash表节点的指针数组 dictEntry **table; //指针数组长度 初始化时候为 DICT_HT_INITIAL_SIZE unsigned long size; //数组长度掩码,用于计算索引值 (size-1) unsigned long sizemask; //hash表节点数量 unsigned long used; } dictht;
字典节点结构:
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; } v; //当前节点指向的后继节点 struct dictEntry *next; } dictEntry;
三个结构的关系图如下(摘自 Redis 设计与实现第一版):
创建字典:
/** * 创建一个新的字典 * dictType *type 字典操作函数类型(type是一个包含一组回调函数的结构) * void *privDataPtr 设置为不同类型回调函数提供的 私有参数 * return 返回一个新的字典 */ dict *dictCreate(dictType *type, void *privDataPtr) { //分配空间 dict *d = zmalloc(sizeof(*d)); //初始化字典的值 _dictInit(d,type,privDataPtr); //返回字典 return d; } /** * 对新的字典进行初始化操作 对字典中的hashtable进行重置操作,并对其他属性设置默认值 私有函数 * dict *d 需要初始化操作的字典 * dictType *type 字典操作函数类型(type是一个包含一组回调函数的结构) * void *privDataPtr 设置为不同类型回调函数提供的 私有参数 * return 返回初始化的状态 */ int _dictInit(dict *d, dictType *type, void *privDataPtr) { //将两个hash表的值设置为NULL或0进行初始化 _dictReset(&d->ht[0]); _dictReset(&d->ht[1]); //设置当前字典的回调函数 d->type = type; d->privdata = privDataPtr; //设置rehash为-1表示没有进行rehash操作 d->rehashidx = -1; //安全迭代数量为0,表示当前没有安全迭代操作 d->iterators = 0; return DICT_OK; }
通过设置type属性,来决定在针对字典节点不同类型的key和val,使用不同类型的方式去处理,比如 val 如果是个字符串,在dictType中释放val空间的回调函数将会调用sdsfree函数,这样来保证,字典对任何类型的数据都可以进行操作。
每次创建字典的时候Redis会分配 dict 结构和 dictht 结构的空间,并初始化其值,创建成功后返回字典结构。
向字典中加入新的节点:
/** * 向hashtable中加入一个新的节点 * dict *d 加入节点的字典 * void *key 节点的key * void *val 节点的value * return 返回 成功或失败 */ int dictAdd(dict *d, void *key, void *val) { //根据key创建一个新的节点 dictEntry *entry = dictAddRaw(d,key); if (!entry) return DICT_ERR; //给新的节点设置val dictSetVal(d, entry, val); return DICT_OK; } /** * 创建一个包含KEY的dictEntry对象,如果正在进行Rehash,会执行一个桶的Rehash操作(在检查key的时候会判断是否处罚rehash) * dict *d 加入节点的字典 * void *key 节点的key * return 返回创建成功包含key的dictEntry */ dictEntry *dictAddRaw(dict *d, void *key) { int index; dictEntry *entry; dictht *ht; //如果正在进行rehash操作,那么执行1次rehash操作 if (dictIsRehashing(d)) _dictRehashStep(d); //获取key所对应的桶的索引值,如果已经存在则返回-1 if ((index = _dictKeyIndex(d, key)) == -1) return NULL; //如果正在进行rehash则将心的值写入到ht[1]中 ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; //分配内存 entry = zmalloc(sizeof(*entry)); //将值加入到hash表每个桶的首位置,并设置其后继节点为之前桶的第一个节点 entry->next = ht->table[index]; ht->table[index] = entry; //节点数量增加 ht->used++; //设置key dictSetKey(d, entry, key); //返回节点结构 return entry; }
在增加新节点的时候,首先会判断是否进行rehash操作,如果正在进行,则进行一次rehash操作。
之后,系统会根据 key 的值,和dictType配置的hash函数,对key进行hash计算,将hash结果与sizemask进行与运算,来判断分布到哪个桶里。
确定桶的索引后,创建节点结构,分配内存,并将其放入指定桶的首位置,这样如果hash值碰撞,将与其他产生碰撞的的节点通过next串联成一个链表。
添加节点的时候,如果正在进行rehash,则将会把新的节点放到ht[1]中。
根据key删除字典中的某个节点:
/** * 查找并在hashtable删除某个key所对应的结构 * dict *d 加入节点的字典 * void *key 要删除的节点的key * int nofree 如果为1 则删除的某个key的结构不进行内存释放 * return 返回 成功或失败 */ 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结果 if (dictIsRehashing(d)) _dictRehashStep(d); //根据用户在dictType中设置的hash函数,获取key的hash值 h = dictHashKey(d, key); //遍历ht[0] 和 ht[1],rehash过程中,节点会分别分布在这两个hash表中 for (table = 0; table <= 1; table++) { //根据hash与sizemast进行与运算(与取模作用相同但效率更高),得到hash桶的索引 idx = h & d->ht[table].sizemask; //获取桶内第一个节点 he = d->ht[table].table[idx]; prevHe = NULL; //遍历桶内的每个节点 while(he) { //比较桶内节点的key与要删除的key是否相同 if (dictCompareKeys(d, key, he->key)) { //解除找到节点在桶内的关系 if (prevHe) prevHe->next = he->next; else d->ht[table].table[idx] = he->next; //如果nofree为false不需要进行释放操作,否则释放key和val的空间 if (!nofree) { dictFreeKey(d, he); dictFreeVal(d, he); } //释放节点空间 zfree(he); //节点数量减1 d->ht[table].used--; return DICT_OK; } prevHe = he; he = he->next; } //如果没有进行rehash则不进行ht[1]的查找 if (!dictIsRehashing(d)) break; } return DICT_ERR; /* not found */ }
字典的迭代:
typedef struct dictIterator { dict *d; //当前的hash表结构,当前进行到的节点位置,是否是安全迭代的标记 int table, index, safe; //当前节点和后继节点 dictEntry *entry, *nextEntry; //字典的指纹信息 long long fingerprint; } dictIterator;
创建迭代结构:
dictIterator *dictGetIterator(dict *d) { //分配内存 dictIterator *iter = zmalloc(sizeof(*iter)); //设置迭代的字典 iter->d = d; //默认查找ht[0] iter->table = 0; //默认 索引为 -1 iter->index = -1; //不进行安全迭代 iter->safe = 0; iter->entry = NULL; iter->nextEntry = NULL; return iter; }
迭代操作:
//创建字典 dict *d = dictCreate(&type, NULL); //创建迭代器对象 dictIterator *di = dictGetIterator(d); dictEntry *de; //加入三个节点 dictAdd(d, "logbird", "123234123"); dictAdd(d, "logbird2", "23434123"); dictAdd(d, "logbird3", "123452453"); //遍历每一个桶 while ((de = dictNext(di))) { //遍历桶内的每一个节点 do { printf("KEY: %s VALUE: %s\n", dictGetKey(de), dictGetVal(de)); } while((de = de->next)); } dictReleaseIterator(di); dictRelease(d);
rehash桶的扩展和字典缩短操作:
执行扩展的条件,在进行一些操作的时候会进行rehash条件检测,代码如下:
static int _dictExpandIfNeeded(dict *d) { //如果正在进行rehash则退出 if (dictIsRehashing(d)) return DICT_OK; //如果当前桶的个数为0,则扩展到 DICT_HT_INITIAL_SIZE 个 if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); //如果节点数大于桶的个数并且(dict_can_resize 为真或节点数与桶个数的比大于dict_force_resize_ratio) //dict_force_resize_ratio 默认为 5 则进行rehash操作,桶个数为当前节点数的二倍 if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) { return dictExpand(d, d->ht[0].used*2); } return DICT_OK; }
如果发现需要进行扩展,则进行扩展操作:
int dictExpand(dict *d, unsigned long size) { dictht n; //通过 节点数 计算得出需要扩展的桶的实际个数(2的n次方大于size的最小值—) unsigned long realsize = _dictNextPower(size); //如果扩展桶的个数小于当前节点数则返回错误 if (dictIsRehashing(d) || d->ht[0].used > size) return DICT_ERR; //分配ht[1]空间并赋予默认值 n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; //如果ht[0]是空的,直接将扩展后的hash表设置为ht[0] if (d->ht[0].table == NULL) { d->ht[0] = n; return DICT_OK; } //设置rehashidx为0标记rehash的开始,并将ht[1]指向新创建的hash表 d->ht[1] = n; d->rehashidx = 0; return DICT_OK; }
Redis在执行一些操作的时候进行渐进式rehash,例如删除操作,添加节点操作等。
static void _dictRehashStep(dict *d) { //如果当前没有进行安全迭代,则对一个桶进行rehash操作 if (d->iterators == 0) dictRehash(d,1); } int dictRehash(dict *d, int n) { //如果没有进行rehash则退出,rehashidx 大于 -1的时候表示正在进行rehash if (!dictIsRehashing(d)) return 0; //n表示进行rehash的桶的个数 while(n--) { dictEntry *de, *nextde; //如果ht[0]里没有节点表示rehash结束 //释放ht[0]的hash表,并重置 //同时将ht[0] 指向 ht[1] //设置rehashidx 为 -1,标志rehash结束 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; } assert(d->ht[0].size > (unsigned)d->rehashidx); //过滤所有为空的桶 while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++; //获取当前获取到的第一个不为NULL的桶 de = d->ht[0].table[d->rehashidx]; //遍历桶里每一个节点 while(de) { unsigned int h; //保存下一个节点 nextde = de->next; //根据key获得hash值,并与ht[1]的sizemask进行与运算,得到新的桶的索引 h = dictHashKey(d, de->key) & d->ht[1].sizemask; //将节点加入到ht[1]指定桶的首位 de->next = d->ht[1].table[h]; d->ht[1].table[h] = de; //ht[0]的节点数减一 d->ht[0].used--; //ht[1]的节点数加一 d->ht[1].used++; //将当前节点设置为下一个 de = nextde; } //将当前桶设置为NULL d->ht[0].table[d->rehashidx] = NULL; //遍历的rehash索引加一 d->rehashidx++; } return 1; }
Redis2.8.9源码 src/dict.h src/dict.c
Redis 设计与实现(第一版)