最近在复习的时候,研究了下关于redis为何rehash对redis的性能影响小,缘由之一在于它的增量式复制,也叫渐进式hash吧!其实这种思想很值得借鉴,分清轻重优化选择redis
/* 哈希表节点 */ typedef struct dictEntry { // 键 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点,造成链表 struct dictEntry *next; } dictEntry; /* This is our hash table structure. Every dictionary has two of this as we * implement incremental rehashing, for the old to the new table. */ /* 哈希表 * 每一个字典都使用两个哈希表,以实现渐进式 rehash 。 */ typedef struct dictht { // 哈希表数组 // 能够看做是:一个哈希表数组,数组的每一个项是entry链表的头结点(链地址法解决哈希冲突) dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码,用于计算索引值 // 老是等于 size - 1 unsigned long sizemask; // 该哈希表已有节点的数量 unsigned long used; } dictht; /* 字典 */ typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash 索引 // 当 rehash 不在进行时,值为 -1 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 目前正在运行的安全迭代器的数量 int iterators; /* number of iterators currently running */ } dict;
dict的结构大体如上,接下来分析一下其中最重要的几个数据成员:数组
dictht::table
:哈希表内部的table结构使用了链地址法来解决哈希冲突,刚开始看的时候我很奇怪,这怎么是个二维数组?这实际上是一个指向数组的指针,数组中的每一项都是entry链表的头结点。dictht ht[2]
:在dict的内部,维护了两张哈希表,做用等同因而一对滚动数组,一张表是旧表,一张表是新表,当hashtable的大小须要动态改变的时候,旧表中的元素就往新开辟的新表中迁移,当下一次变更大小,当前的新表又变成了旧表,以此达到资源的复用和效率的提高。安全
字段rehashidx
:由于是渐进式的哈希,数据的迁移并非一步完成的,因此须要有一个索引来指示当前的rehash进度。当rehashidx为-1时,表明没有哈希操做。数据结构
rehash
的主体部分:函数
/* Performs N steps of incremental rehashing. Returns 1 if there are still * keys to move from the old to the new hash table, otherwise 0 is returned. * * Note that a rehashing step consists in moving a bucket (that may have more * than one key as we use chaining) from the old to the new hash table, however * since part of the hash table may be composed of empty spaces, it is not * guaranteed that this function will rehash even a single bucket, since it * will visit at max N*10 empty buckets in total, otherwise the amount of * work it does would be unbound and the function may block for a long time. * rehash是以bucket(桶)为基本单位进行渐进式的数据迁移的,每步完成一个bucket的迁移,直至全部数据迁移完毕。一个bucket对应哈希表数组中的一条entry链表。新版本的dictRehash()还加入了一个最大访问空桶数(empty_visits)的限制来进一步减少可能引发阻塞的时间。 */ int dictRehash(dict *d, int n) { int empty_visits = n*10; /* Max number of empty buckets to visit. */ 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; } de = d->ht[0].table[d->rehashidx]; /* Move all the keys in this bucket from the old to the new hash HT */ while(de) { uint64_t h; nextde = de->next; /* Get the index in the new hash table */ 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++; } /* Check if we already rehashed the whole table... */ 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; }
接下来咱们深扒一下这个函数的具体实现。性能
渐进式哈希
的精髓在于:数据的迁移不是一次性完成的,而是能够经过dictRehash()这个函数分步规划的,而且调用方能够及时知道是否须要继续进行渐进式哈希操做。若是dict数据结构中存储了海量的数据,那么一次性迁移势必带来redis性能的降低,别忘了redis是单线程模型,在实时性要求高的场景下这多是致命的。而渐进式哈希则将这种代价可控地分摊了,调用方能够在dict作插入,删除,更新的时候执行dictRehash(),最小化数据迁移的代价。
在迁移的过程当中,数据是在新表仍是旧表中并非一个很是急迫的需求,迁移的过程并不会丢失数据,在旧表中找不到再到新表中寻找就是了。
参考博客:https://blog.csdn.net/cqk0100/article/details/8040081优化