baiyanphp
所有视频:【每日学习记录】使用录像设备记录天天的学习redis
dict,即字典,也被称为哈希表hashtable。在redis的五大数据结构中,有以下两种情形会使用dict结构:算法
- hash:数据量小的时候使用ziplist,量大时使用dict
- zset:数据量小的时候使用ziplist,数据量大的时候使用skiplist + dict
结合以上两种状况,咱们能够看出,dict也是一种较为复杂的数据结构,一般用在数据量大的情形中。一般状况下,一个dict长这样:
在这个哈希表中,每一个存储单元被称为一个桶(bucket)。咱们向这个dict(hashtable)中插入一个"name" => "baiyan"的key-value对,假设对这个key “name”作哈希运算结果为3,那么咱们寻找这个hashtable中下标为3的位置并将其插入进去,获得如图所示的情形。咱们能够看到,dict最大的优点就在于其查找的时间复杂度为O(1),是任何其它数据结构所不能比拟的。咱们在查找的时候,首先对key ”name“进行哈希运算,获得结果3,咱们直接去dict索引为3的位置进行查找,便可获得value ”baiyan“,时间复杂度为O(1),是至关快的。编程
在redis中,在普通字典的基础上,为了方便进行扩容与缩容,增长了一些描述字段。仍是以上面的例子为基础,在redis中存储结构以下图所示:
dictht的结构以下:segmentfault
typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht;
在dictht中,真正存储数据的地方是**table这个dictEntry类型二级指针。咱们能够把它拆分来看,首先第一个指针能够表明一个一维数组,即哈希表。然后面的指针表明,在每一个一维数组(哈希表)的存储单元中,存储的都是一个dictEntry类型的指针,这个指针就指向咱们存储key-value对的dictEntry类型结构的所在位置,如上图所示。
存储最终key-value对的dictEntry的结构以下:数组
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry;
一个存储key-value对的entry,最主要仍是这里的key和value字段。因为存储在dict中的key和value能够是字符串、也能够是整数等等,因此在这里均用一个void * 指针来表示。咱们注意到最后有一个也是同类型dictEntry的next指针,它就是用来解决咱们常常说的哈希冲突问题。数据结构
当咱们对不一样的key进行哈希运算以后结果相同时,就碰到了哈希冲突的问题。经常使用的两种哈希冲突的解决方案有两种:开放定址法与链地址法。redis使用的是后者。经过这个next指针,咱们就能够将哈希值相同的元素都串联起来,解决哈希冲突的问题。注意在redis的源码实现中,在往dict插入元素的时使用的是链表的头插法,即将新元素插到链表的头部,这样就不用每次遍历到链表的末尾进行插入,下降了插入的时间复杂度。架构
假设咱们一直往dict中插入元素,那么这个哈希表的全部bucket都会被占满,并且在链地址法解决哈希冲突的过程当中,每一个bucket后面的链表会很是长。这样一来,这个链表的时间复杂度就会逐渐退化成O(n)。对于总体的dict而言,其查询效率就会大大下降。为了解决数据量过大致使dict性能降低的问题,咱们须要对其进行扩容,来知足后续插入元素的存储须要。性能
typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; /* rehash进程标识。若是值为-1则不在rehash,不然在进行rehash */ unsigned long iterators; /* number of iterators currently running */ } dict;
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]; /* 将老的哈希表ht[0]中的元素移动到新哈希表ht[1]中 */ while(de) { uint64_t h; nextde = de->next; /* 计算新哈希表ht[1]的索引下标*/ 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完成,若完成则置rehashidx为-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; }
若是在rehash的过程当中(例如容量由4扩容到8),若是须要查找一个元素。首先咱们会计算哈希值(假设为3)去找老的哈希表ht[0],若是咱们发现位置3上已经没有了元素,说明这个元素已经被rehash过了,到新的哈希表上对应的位置3或7上寻找便可。学习
试想这么一种状况:在rehash以前,咱们使用SCAN命令对dict进行第一次遍历;而rehash结束以后咱们进行第二次SCAN遍历,会发生什么状况?
在讨论这个问题以前,咱们先熟悉一下SCAN命令。咱们知道在咱们执行keys这种返回全部key值的命令,因为全部key加在一块是至关多的,若是一次性所有把它遍历完成,可以让单进程的redis阻塞至关长的时间,在这段时间里都没法对外提供服务。为了解决这个问题,SCAN命令横空出世。它并非一次性将全部的key都返回,而是每次返回一部分key并记录一下当前遍历的进度,这里用一个游标去记录。下次再次运行SCAN命令的时候,redis会从游标的位置开始继续往下遍历。SCAN命令实际上也是一种分而治之的思想,这样一次遍历一小部分,直到遍历完成。SCAN命令官方解释以下:
SCAN 命令是一个基于游标的 迭代器: SCAN 命令每次被调用以后, 都会向用户返回一个新的游标,用户在下次迭代时须要使用这个新游标做为 SCAN 命令的游标参数, 以此来延续以前的迭代过程。
SCAN命令的使用方法以下:
redis 127.0.0.1:6379> scan 0 1) "17" 2) 1) "key:12" 2) "key:8" 3) "key:4" 4) "key:14" 5) "key:16" 6) "key:17" 7) "key:15" 8) "key:10" 9) "key:3" 10) "key:7" 11) "key:1" redis 127.0.0.1:6379> scan 17 1) "0" 2) 1) "key:5" 2) "key:18" 3) "key:0" 4) "key:2" 5) "key:19" 6) "key:13" 7) "key:6" 8) "key:9" 9) "key:11"
- 在上面这个例子中,第一次迭代使用0做为游标,表示开始一次新的迭代。第二次迭代使用的是第一次迭代时返回的游标,也便是命令回复第一个元素的值17 。
- 从上面的示例能够看到, SCAN 命令的回复是一个包含两个元素的数组,第一个数组元素是用于进行下一次迭代的新游标,而第二个数组元素则是一个数组,这个数组中包含了全部被迭代的元素。
- 在第二次调用 SCAN 命令时,命令返回了游标0,这表示迭代已经结束,整个数据集(collection)已经被完整遍历过了。
- 以0做为游标开始一次新的迭代,一直调用 SCAN 命令,直到命令返回游标0,咱们称这个过程为一次完整遍历。
回到正题,咱们来解决以前的问题。 咱们简化一下dict的结构,只留下两个基本的哈希表结构,咱们如今有4个元素:十二、1三、1四、15,假设哈希算法为取余。
那么咱们将两次SCAN的结果合起来,为十二、十二、1三、1四、15。咱们发现,元素12被多遍历了一次,与咱们的预期不符。因此咱们得出结论:在rehash过程当中执行SCAN命令会致使遍历结果出现冗余。
为了解决扩容和缩容进行rehash的过程当中重复遍历的问题,redis对哈希表的下标作出了以下变化(v就是哈希表的下标):
v = rev(v); v++; v = rev(v);
首先将游标倒置,加一后,再倒置,也就是咱们所说的“高位++”的操做。这里的这几步操做是来经过前一个下标,计算出哈希表下一个bucket的下标。举一个例子:最开始00这个bucket不用动,以前通过正常的低位++以后,00的后面应该为01。然而如今是高位++,原来01的位置的下标就会变成10.......以此类推。最终,哈希表的下标就会由原来顺序的00、0一、十、11变成了00、十、0一、11,如图所示:
这样就可以保证咱们屡次执行SCAN命令就不会重复遍历了吗?接下来就是见证奇迹的时刻:
开始进行rehash
unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, dictScanBucketFunction* bucketfn, void *privdata) { dictht *t0, *t1; const dictEntry *de, *next; unsigned long m0, m1; if (dictSize(d) == 0) return 0; // 若是SCAN的时候没有进行rehash if (!dictIsRehashing(d)) { t0 = &(d->ht[0]); m0 = t0->sizemask; /* Emit entries at cursor */ if (bucketfn) bucketfn(privdata, &t0->table[v & m0]); de = t0->table[v & m0]; while (de) { //遍历同一个bucket上后面挂接的链表 next = de->next; fn(privdata, de); de = next; } /* Set unmasked bits so incrementing the reversed cursor * operates on the masked bits */ v |= ~m0; /* Increment the reverse cursor */ v = rev(v); //反转v v++; //反转以后即为高位++ v = rev(v); //再反转回来,获得下一个游标值 // 若是SCAN的时候正在进行rehash } else { t0 = &d->ht[0]; t1 = &d->ht[1]; /* Make sure t0 is the smaller and t1 is the bigger table */ if (t0->size > t1->size) { t0 = &d->ht[1]; t1 = &d->ht[0]; } m0 = t0->sizemask; m1 = t1->sizemask; /* Emit entries at cursor */ if (bucketfn) bucketfn(privdata, &t0->table[v & m0]); de = t0->table[v & m0]; while (de) { //遍历同一个bucket上后面挂接的链表 next = de->next; fn(privdata, de); de = next; } /* Iterate over indices in larger table that are the expansion * of the index pointed to by the cursor in the smaller table */ do { /* Emit entries at cursor */ if (bucketfn) bucketfn(privdata, &t1->table[v & m1]); de = t1->table[v & m1]; while (de) { //遍历同一个bucket上后面挂接的链表 next = de->next; fn(privdata, de); de = next; } /* Increment the reverse cursor not covered by the smaller mask.*/ v |= ~m1; v = rev(v); //反转v v++; //反转以后即为高位++ v = rev(v); //再反转回来,获得下一个游标值 /* Continue while bits covered by mask difference is non-zero */ } while (v & (m0 ^ m1)); } return v; }
有关rehash过程对SCAN的影响,限于篇幅仅仅展现这种状况。更多的情形请参考:Redis scan命令原理