Dict在redis中是最为核心的一个数据结构,由于它承载了redis里的全部数据,你能够简单粗暴的认为redis就是一个大的dict,里面存储的全部的key-value。html
redis中dict的本质其实就是一个hashtable,因此它也须要考虑全部hashtable全部的问题,如何组织K-V、如何处理hash冲突、扩容策略及扩容方式……。实际上Redis中hashtable的实现方式就是普通的hashtable,但Redis创新的引入了渐进式hash以减少hashtable扩容是对性能带来的影响,接下来咱们就来看看redis中hashtable的具体实现。java
dict的定义在dict.h中,其各个字段及其含义以下:git
typedef struct dict { dictType *type; // dictType结构的指针,封装了不少数据操做的函数指针,使得dict能处理任意数据类型(相似面向对象语言的interface,能够重载其方法) void *privdata; // 一个私有数据指针(privdata),由调用者在建立dict的时候传进来。 dictht ht[2]; // 两个hashtable,ht[0]为主,ht[1]在渐进式hash的过程当中才会用到。 long rehashidx; /* 增量hash过程过程当中记录rehash执行到第几个bucket了,当rehashidx == -1表示没有在作rehash */ unsigned long iterators; /* 正在运行的迭代器数量 */ } dict;
重点介绍下dictType *type字段(我的感受命名为type不太合适),其做用就是为了让dict支持各类数据类型,由于不一样的数据类型须要对应不一样的操做函数,好比计算hashcode 字符串和整数的计算方式就不同, 因此dictType经过函数指针的方式,将不一样数据类型的操做都封装起来。从面相对象的角度来看,能够把dictType当成dict中各类数据类型相关操做的interface,各个数据类型只须要实现其对应的数据操做就行。 dictType中封装了如下几个函数指针。github
typedef struct dictType { uint64_t (*hashFunction)(const void *key); // 对key生成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;
dict中还有另一个重要的字段dictht ht[2],dictht其实就是hashtable,但这里为何是ht[2]? 这就不得不提到redis dict的渐进式hash,dict的hashtable的扩容不是一次性完成的,它是先创建一个大的新的hashtable存放在ht[1]中,而后逐渐把ht[0]的数据迁移到ht[1]中,rehashidx就是ht[0]中数据迁移的进度,渐进式hash的过程会在后文中详解。redis
这里咱们来看下dictht的定义:编程
typedef struct dictht { dictEntry **table; // hashtable中的连续空间 unsigned long size; // table的大小 unsigned long sizemask; // hashcode的掩码 unsigned long used; // 已存储的数据个数 } dictht;
其中dictEntry就是对dict中每对key-value的封装,除了具体的key-value,其还包含一些其余信息,具体以下:api
typedef struct dictEntry { void *key; union { // dictEntry在不一样用途时存储不一样的数据 void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; // hash冲突时开链,单链表的next指针 } dictEntry;
dict中的hashtable在出现hash冲突时采用的是开链方式,若是有多个entry落在同一个bucket中,那么他们就会串成一个单链表存储。数据结构
若是咱们将dict在内存中的存储绘制出来,会是下图这个样子。
dom
在看dict几个核心API实现以前,咱们先来看下dict的扩容,也就是redis的渐进式hash。 何为渐进式hash?redis为何采用渐进式hash?渐进式hash又是如何实现的?函数
要回答这些问题,咱们先来考虑下hashtable扩容的过程。若是熟悉java的同窗可能知道,java中hashmap的扩容是在数据元素达到某个阈值后,新建一个更大的空间,一次性把旧数据搬过去,搬完以后再继续后续的操做。若是数据量过大的话,HashMap扩容是很是耗时的,全部有些编程规范推荐new HashMap时最好指定其容量,防止出现自动扩容。
可是redis在新建dict的时候,无法知道数据量大小,若是直接采用java hashmap的扩容方式,由于redis是单线程的,势必在扩容过程当中啥都干不了,阻塞掉后面的请求,最终影响到整个redis的性能。如何解决? 其实也很简单,就是化整为零,将一次大的扩容操做拆分红屡次小的步骤,一步步来减小扩容对其余操做的影响,其具体实现以下:
上文中咱们已经看到了在dict的定义中有个dictht ht[2],dict在扩容过程当中会有两个hashtable分别存储在ht[0]和ht[1]中,其中ht[0]是旧的hashtable,ht[1]是新的更大的hashtable。
/* 检查是否dict须要扩容 */ static int _dictExpandIfNeeded(dict *d) { /* 已经在渐进式hash的流程中了,直接返回 */ if (dictIsRehashing(d)) return DICT_OK; /* If the hash table is empty expand it to the initial size. */ if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); /* 当配置了可扩容时,容量负载达到100%就扩容。配置不可扩容时,负载达到5也会强制扩容*/ 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; }
Redis在每次查找某个key的索引下标时都会检查是否须要对ht[0]作扩容,若是配置的是能够扩容 那么当hashtable使用率超过100%(uesed/size)就触发扩容,不然使用率操做500%时强制扩容。执行扩容的代码以下:
/* dict的建立和扩容 */ int dictExpand(dict *d, unsigned long size) { /* 若是size比hashtable中的元素个数还小,那size就是无效的,直接返回error */ if (dictIsRehashing(d) || d->ht[0].used > size) return DICT_ERR; dictht n; /* 新的hashtable */ // 扩容时新table容量是大于当前size的最小2的幂次方,但有上限 unsigned long realsize = _dictNextPower(size); // 若是新容量和旧容量一致,没有必要继续执行了,返回err if (realsize == d->ht[0].size) return DICT_ERR; /* 新建一个容量更大的hashtable */ n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; // 若是是dict初始化的状况,直接把新建的hashtable赋值给ht[0]就行 if (d->ht[0].table == NULL) { d->ht[0] = n; return DICT_OK; } // 非初始化的状况,将新表赋值给ht[1], 而后标记rehashidx 0 d->ht[1] = n; d->rehashidx = 0; // rehashidx表示当前rehash到ht[0]的下标位置 return DICT_OK; }
这里dictExpand只是建立了新的空间,将rehashidx标记为0(rehashidx==-1表示不在rehash的过程当中),并未对ht[0]中的数据迁移到ht[1]中。数据迁移的逻辑都在_dictRehashStep()中。 _dictRehashStep()是只迁移一个bucket,它在dict的查找、插入、删除的过程当中都会被调到,每次调用至少迁移一个bucket。 而dictRehash()是_dictRehashStep()的具体实现,代码以下:
/* redis渐进式hash,采用分批的方式,逐渐将ht[0]依下标转移到ht[2],避免了hashtable扩容时大量 * 数据迁移致使的性能问题 * 参数n是指此次rehash只作n个bucket */ int dictRehash(dict *d, int n) { int empty_visits = n*10; /* 最大空bucket数量,若是遇到empty_visits个空bucket,直接结束当前rehash的过程 */ if (!dictIsRehashing(d)) return 0; 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; // 若是遇到了empty_visits个空的bucket,直接结束 } // 遍历当前bucket中的链表,直接将其移动到新的hashtable中 de = d->ht[0].table[d->rehashidx]; /* 把全部的key从旧的hash桶移到新的hash桶中 */ while(de) { uint64_t h; nextde = de->next; /* 获取到key在新hashtable中的下标 */ 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++; } /* 检测是否已对全表作完了rehash */ if (d->ht[0].used == 0) { zfree(d->ht[0].table); // 释放旧ht所占用的内存空间 d->ht[0] = d->ht[1]; // ht[0]始终是在用ht,ht[1]始终是新ht,ht0全迁移到ht1后会交换下 _dictReset(&d->ht[1]); d->rehashidx = -1; return 0; // 若是全表hash完,返回0 } /* 还须要继续作hash返回1 */ return 1; }
能够看出,rehash就是分批次把ht[0]中的数据搬到ht[1]中,这样将原有的一个大操做拆分为不少个小操做逐步进行,避免了redis发生dict扩容是瞬时不可用的状况,缺点是在redis扩容过程当中会占用俩份存储空间,并且占用时间会比较长。
/* 向dict中添加元素 */ int dictAdd(dict *d, void *key, void *val) { dictEntry *entry = dictAddRaw(d,key,NULL); // if (!entry) return DICT_ERR; dictSetVal(d, entry, val); return DICT_OK; } /* 添加和查找的底层实现: * 这个函数只会返回key对应的entry,并不会设置key对应的value,而是把设值权交给调用者。 * * 这个函数也做为一个API直接暴露给用户调用,主要是为了在dict中存储非指针类的数据,好比 * entry = dictAddRaw(dict,mykey,NULL); * if (entry != NULL) dictSetSignedIntegerVal(entry,1000); * * 返回值: * 若是key已经存在于dict中了,直接返回null,并把已经存在的entry指针放到&existing里。不然 * 为key新建一个entry并返回其指针。 */ dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) { long index; dictEntry *entry; dictht *ht; if (dictIsRehashing(d)) _dictRehashStep(d); /* 获取到新元素的下标,若是返回-1标识该元素已经存在于dict中了,直接返回null */ if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1) return NULL; /* 不然就给新元素分配内存,并将其插入到链表的头部(通常新插入的数据被访问的频次会更高)*/ ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; entry = zmalloc(sizeof(*entry)); entry->next = ht->table[index]; ht->table[index] = entry; ht->used++; /* 若是是新建的entry,须要把key填进去 */ dictSetKey(d, entry, key); return entry; }
插入过程也比较简单,就是先定位bucket的下标,而后插入到单链表的头节点,注意这里也须要考虑到rehash的状况,若是是在rehash过程当中,新数据必定是插入到ht[1]中的。
dictEntry *dictFind(dict *d, const void *key) { dictEntry *he; uint64_t h, idx, table; if (dictSize(d) == 0) return NULL; /* dict为空 */ if (dictIsRehashing(d)) _dictRehashStep(d); h = dictHashKey(d, key); // 查找的过程当中,可能正在rehash中,因此新老两个hashtable都须要查 for (table = 0; table <= 1; table++) { idx = h & d->ht[table].sizemask; he = d->ht[table].table[idx]; while(he) { if (key==he->key || dictCompareKeys(d, key, he->key)) return he; he = he->next; } // 若是ht[0]中没找到,且再也不rehas中,就不须要继续找了ht[1]了。 if (!dictIsRehashing(d)) return NULL; } return NULL; }
查找的过程比较简单,就是用hashcode作定位,而后遍历单链表。但这里须要考虑到若是是在rehash过程当中,可能须要查找ht[2]中的两个hashtable。
/* 查找并删除一个元素,是dictDelete()和dictUnlink()的辅助函数。*/ static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) { uint64_t h, idx; dictEntry *he, *prevHe; int table; if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL; if (dictIsRehashing(d)) _dictRehashStep(d); h = dictHashKey(d, key); // 这里也是须要考虑到rehash的状况,ht[0]和ht[1]中的数据都要删除掉 for (table = 0; table <= 1; table++) { idx = h & d->ht[table].sizemask; he = d->ht[table].table[idx]; prevHe = NULL; while(he) { if (key==he->key || dictCompareKeys(d, key, he->key)) { /* 从列表中unlink掉元素 */ if (prevHe) prevHe->next = he->next; else d->ht[table].table[idx] = he->next; // 若是nofree是0,须要释放k和v对应的内存空间 if (!nofree) { dictFreeKey(d, he); dictFreeVal(d, he); zfree(he); } d->ht[table].used--; return he; } prevHe = he; he = he->next; } if (!dictIsRehashing(d)) break; } return NULL; /* 没找到key对应的数据 */ }
其余的API实现都比较简单,我在dict.c源码中作了大量的注释,有兴趣能够自行阅读下,我这里仅列举并说明下其大体的功能。
dict *dictCreate(dictType *type, void *privDataPtr); // 建立dict int dictExpand(dict *d, unsigned long size); // 扩缩容 int dictAdd(dict *d, void *key, void *val); // 添加k-v dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing); // 添加的key对应的dictEntry dictEntry *dictAddOrFind(dict *d, void *key); // 添加或者查找 int dictReplace(dict *d, void *key, void *val); // 替换key对应的value,若是没有就添加新的k-v int dictDelete(dict *d, const void *key); // 删除某个key对应的数据 dictEntry *dictUnlink(dict *ht, const void *key); // 卸载某个key对应的entry void dictFreeUnlinkedEntry(dict *d, dictEntry *he); // 卸载并清除key对应的entry void dictRelease(dict *d); // 释放整个dict dictEntry * dictFind(dict *d, const void *key); // 数据查找 void *dictFetchValue(dict *d, const void *key); // 获取key对应的value int dictResize(dict *d); // 重设dict的大小,主要是缩容用的 /************ 迭代器相关 *********** */ dictIterator *dictGetIterator(dict *d); dictIterator *dictGetSafeIterator(dict *d); dictEntry *dictNext(dictIterator *iter); void dictReleaseIterator(dictIterator *iter); /************ 迭代器相关 *********** */ dictEntry *dictGetRandomKey(dict *d); // 随机返回一个entry dictEntry *dictGetFairRandomKey(dict *d); // 随机返回一个entry,但返回每一个entry的几率会更均匀 unsigned int dictGetSomeKeys(dict *d, dictEntry **des, unsigned int count); // 获取dict中的部分数据
本文是Redis源码剖析系列博文,同时也有与之对应的Redis中文注释版,有想深刻学习Redis的同窗,欢迎star和关注。
Redis中文注解版仓库:https://github.com/xindoo/Redis
Redis源码剖析专栏:https://zxs.io/s/1h
若是以为本文对你有用,欢迎一键三连。
本文来自https://blog.csdn.net/xindoo