Redis源码解析——字典遍历

来源:互联网 发布:淘宝卖家信息采集软件 编辑:程序博客网 时间:2024/04/30 14:50
之前两篇博文讲解了字典库的基础,本文将讲解其遍历操作。之所以将遍历操作独立成一文来讲,是因为其中的内容和之前的基本操作还是有区别的。特别是高级遍历一节介绍的内容,充满了精妙设计的算法智慧。(转载请指明出于breaksoftware的csdn博客)

迭代器遍历

        由于Redis字典库有rehash机制,而且是渐进式的,所以迭代器操作可能会通过其他特殊方式来实现,以保证能遍历到所有数据。但是阅读完源码发现,其实这个迭代器是个受限的迭代器,实现方法也很简单。我们先看下其基础结构:
[cpp] view plain copy
  1. typedef struct dictIterator {  
  2.     dict *d;  
  3.     long index;  
  4.     int table, safe;  
  5.     dictEntry *entry, *nextEntry;  
  6.     /* unsafe iterator fingerprint for misuse detection. */  
  7.     long long fingerprint;  
  8. } dictIterator;  
        成员变量d指向迭代器处理的字典。index是dictht中table数组的下标。table是dict结构中dictht数组的下标,即标识ht[0]还是ht[1]。safe字段用于标识该迭代器是否为一个安全的迭代器。如果是,则可以在迭代过程中使用dictDelete、dictFind等方法;如果不是,则只能使用dictNext遍历方法。entry和nextEntry分别指向当前的元素和下一个元素。fingerprint是字典的指纹,我们可以先看下指纹算法的实现:
[cpp] view plain copy
  1. long long dictFingerprint(dict *d) {  
  2.     long long integers[6], hash = 0;  
  3.     int j;  
  4.   
  5.     integers[0] = (long) d->ht[0].table;  
  6.     integers[1] = d->ht[0].size;  
  7.     integers[2] = d->ht[0].used;  
  8.     integers[3] = (long) d->ht[1].table;  
  9.     integers[4] = d->ht[1].size;  
  10.     integers[5] = d->ht[1].used;  
  11.   
  12.     /* We hash N integers by summing every successive integer with the integer 
  13.      * hashing of the previous sum. Basically: 
  14.      * 
  15.      * Result = hash(hash(hash(int1)+int2)+int3) ... 
  16.      * 
  17.      * This way the same set of integers in a different order will (likely) hash 
  18.      * to a different number. */  
  19.     for (j = 0; j < 6; j++) {  
  20.         hash += integers[j];  
  21.         /* For the hashing step we use Tomas Wang's 64 bit integer hash. */  
  22.         hash = (~hash) + (hash << 21); // hash = (hash << 21) - hash - 1;  
  23.         hash = hash ^ (hash >> 24);  
  24.         hash = (hash + (hash << 3)) + (hash << 8); // hash * 265  
  25.         hash = hash ^ (hash >> 14);  
  26.         hash = (hash + (hash << 2)) + (hash << 4); // hash * 21  
  27.         hash = hash ^ (hash >> 28);  
  28.         hash = hash + (hash << 31);  
  29.     }  
  30.     return hash;  
  31. }  
        可以见得,它使用了ht[0]和ht[1]的相关信息进行Hash运算,从而得到该字典的指纹。我们可以发现,如果dictht的table、size和used任意一个有变化,则指纹将被改变。这也就意味着,扩容、锁容、rehash、新增元素和删除元素都会改变指纹(除了修改元素内容)。
        生成一个迭代器的方法很简单,该字典库提供了两种方式:
[cpp] view plain copy
  1. dictIterator *dictGetIterator(dict *d)  
  2. {  
  3.     dictIterator *iter = zmalloc(sizeof(*iter));  
  4.   
  5.     iter->d = d;  
  6.     iter->table = 0;  
  7.     iter->index = -1;  
  8.     iter->safe = 0;  
  9.     iter->entry = NULL;  
  10.     iter->nextEntry = NULL;  
  11.     return iter;  
  12. }  
  13.   
  14. dictIterator *dictGetSafeIterator(dict *d) {  
  15.     dictIterator *i = dictGetIterator(d);  
  16.   
  17.     i->safe = 1;  
  18.     return i;  
  19. }  
        然后我们看下遍历迭代器的操作。如果是初次迭代,则要查看是否是安全迭代器,如果是安全迭代器则让其对应的字典对象的iterators自增;如果不是则记录当前字典的指纹
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. dictEntry *dictNext(dictIterator *iter)  
  2. {  
  3.     while (1) {  
  4.         if (iter->entry == NULL) {  
  5.             dictht *ht = &iter->d->ht[iter->table];  
  6.             if (iter->index == -1 && iter->table == 0) {  
  7.                 if (iter->safe)  
  8.                     iter->d->iterators++;  
  9.                 else  
  10.                     iter->fingerprint = dictFingerprint(iter->d);  
  11.             }  
        因为要遍历的时候,字典可以已经处于rehash的中间状态,所以还要遍历ht[1]中的元素
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. iter->index++;  
  2. if (iter->index >= (long) ht->size) {  
  3.     if (dictIsRehashing(iter->d) && iter->table == 0) {  
  4.         iter->table++;  
  5.         iter->index = 0;  
  6.         ht = &iter->d->ht[1];  
  7.     } else {  
  8.         break;  
  9.     }  
  10. }  
  11. iter->entry = ht->table[iter->index];  
  12. se {  
  13. iter->entry = iter->nextEntry;  
        往往使用迭代器获得元素后,会让字典删除这个元素,这个时候就无法通过迭代器获取下一个元素了,于是作者设计了nextEntry来记录当前对象的下一个对象指针
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1.         if (iter->entry) {  
  2.             /* We need to save the 'next' here, the iterator user 
  3.              * may delete the entry we are returning. */  
  4.             iter->nextEntry = iter->entry->next;  
  5.             return iter->entry;  
  6.         }  
  7.     }  
  8.     return NULL;  
  9. }  
        遍历完成后,要调用下面方法释放迭代器。需要注意的是,如果是安全迭代器,就需要让其指向的字典的iterators自减以还原;如果不是,则需要检测前后字典的指纹是否一致
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. void dictReleaseIterator(dictIterator *iter)  
  2. {  
  3.     if (!(iter->index == -1 && iter->table == 0)) {  
  4.         if (iter->safe)  
  5.             iter->d->iterators--;  
  6.         else  
  7.             assert(iter->fingerprint == dictFingerprint(iter->d));  
  8.     }  
  9.     zfree(iter);  
  10. }  
        最后我们探讨下什么是安全迭代器。源码中我们看到如果safe为1,则让字典iterators自增,这样dict字典库中的操作就不会触发rehash渐进,从而在一定程度上(消除rehash影响,但是无法阻止用户删除元素)保证了字典结构的稳定。如果不是安全迭代器,则只能使用dictNext方法遍历元素,而像获取元素值的dictFetchValue方法都不能调用。因为dictFetchValue底层会调用_dictRehashStep让字典结构发生改变。
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. static void _dictRehashStep(dict *d) {  
  2.     if (d->iterators == 0) dictRehash(d,1);  
  3. }  
        但是作者在源码说明中说安全迭代器在迭代过程中可以使用dictAdd方法,但是我觉得这个说法是错误的。因为dictAdd方法插入的元素可能在当前遍历的对象之前,这样就在之后的遍历中无法遍历到;也可能在当前遍历的对象之后,这样就在之后的遍历中可以遍历到。这样一种动作,两种可能结果的方式肯定是有问题的。我查了下该库在Redis中的应用,遍历操作不是为了获取值就是为了删除值,而没有增加元素的操作,如
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. void clusterBlacklistCleanup(void) {  
  2.     dictIterator *di;  
  3.     dictEntry *de;  
  4.   
  5.     di = dictGetSafeIterator(server.cluster->nodes_black_list);  
  6.     while((de = dictNext(di)) != NULL) {  
  7.         int64_t expire = dictGetUnsignedIntegerVal(de);  
  8.   
  9.         if (expire < server.unixtime)  
  10.             dictDelete(server.cluster->nodes_black_list,dictGetKey(de));  
  11.     }  
  12.     dictReleaseIterator(di);  
  13. }  

高级遍历

        高级遍历允许ht[0]和ht[1]之间数据在迁移过程中进行遍历,通过相应的算法可以保证所有的元素都可以被遍历到。我们先看下功能的实现:
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. unsigned long dictScan(dict *d,  
  2.                        unsigned long v,  
  3.                        dictScanFunction *fn,  
  4.                        void *privdata)  
        参数d是字典的指针;v是迭代器,这个迭代器初始值为0,每次调用dictScan都会返回一个新的迭代器。于是下次调用这个函数时要传入新的迭代器的值。fn是个函数指针,每遍历到一个元素时,都是用该函数对元素进行操作。
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. typedef void (dictScanFunction)(void *privdata, const dictEntry *de);  
        Redis中这个方法的调用样例是:
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. do {  
  2.     cursor = dictScan(ht, cursor, scanCallback, privdata);  
  3. while (cursor &&  
  4.       maxiterations-- &&  
  5.       listLength(keys) < (unsigned long)count);  
        对于不在rehash状态的字典,则只要对ht[0]中迭代器指向的链表进行遍历就行了
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. dictht *t0, *t1;  
  2. const dictEntry *de;  
  3. unsigned long m0, m1;  
  4.   
  5. if (dictSize(d) == 0) return 0;  
  6.   
  7. if (!dictIsRehashing(d)) {  
  8.     t0 = &(d->ht[0]);  
  9.     m0 = t0->sizemask;  
  10.   
  11.     /* Emit entries at cursor */  
  12.     de = t0->table[v & m0];  
  13.     while (de) {  
  14.         fn(privdata, de);  
  15.         de = de->next;  
  16.     }  
        如果在rehash状态,就要遍历ht[0]和ht[1]。遍历前要确定哪个dictht.table长度短(假定其长度为len=8),先对短的中该迭代器(假定为iter=4)对应的链进行遍历,然后遍历大的。然而不仅要遍历大的dictht中迭代器(iter=4)对应的链,还要遍历比iter大len的迭代器(4+8=12)对应的链表。
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. else {  
  2.     t0 = &d->ht[0];  
  3.     t1 = &d->ht[1];  
  4.   
  5.     /* Make sure t0 is the smaller and t1 is the bigger table */  
  6.     if (t0->size > t1->size) {  
  7.         t0 = &d->ht[1];  
  8.         t1 = &d->ht[0];  
  9.     }  
  10.   
  11.     m0 = t0->sizemask;  
  12.     m1 = t1->sizemask;  
  13.   
  14.     /* Emit entries at cursor */  
  15.     de = t0->table[v & m0];  
  16.     while (de) {  
  17.         fn(privdata, de);  
  18.         de = de->next;  
  19.     }  
  20.   
  21.     /* Iterate over indices in larger table that are the expansion 
  22.      * of the index pointed to by the cursor in the smaller table */  
  23.     do {  
  24.         /* Emit entries at cursor */  
  25.         de = t1->table[v & m1];  
  26.         while (de) {  
  27.             fn(privdata, de);  
  28.             de = de->next;  
  29.         }  
  30.   
  31.         /* Increment bits not covered by the smaller mask */  
  32.         v = (((v | m0) + 1) & ~m0) | (v & m0);  
  33.   
  34.         /* Continue while bits covered by mask difference is non-zero */  
  35.     } while (v & (m0 ^ m1));  
  36. }  
        最后要重新计算下次使用的迭代器并返回
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1.     /* Set unmasked bits so incrementing the reversed cursor 
  2.      * operates on the masked bits of the smaller table */  
  3.     v |= ~m0;  
  4.   
  5.     /* Increment the reverse cursor */  
  6.     v = rev(v);  
  7.     v++;  
  8.     v = rev(v);  
  9.   
  10.     return v;  
  11. }  
        从上面的设计来看,调用dictScan时不能有多线程操作该字典,否则会出现遗漏遍历的情况。但是在每次调用dictScan之间可以对字典进行操作。
        其实这个遍历中最核心的是迭代器v的计算方法,我们只要让v从0开始,执行“或操作”最短ht.table(~m0)大小、二进制翻转、加1、再二进制翻转就可以实现0到~m0的遍历。我们看个例子:

        我一直想不出这套算法为什么能满足这样的特点,还是需要数学大神解释一下。同时也可见这种算法的作者Pieter Noordhuis数学有一定功底。
        关键这样的算法不仅可以完成遍历,还可以在数组大小动态变化时保证元素被全部遍历到。我把代码提炼出来,模拟了长度为8的数组向长度为16的数组扩容,和长度为16的数组向长度为8的数组缩容的过程。为了让问题简单化,我们先不考虑两个数组的问题,只认为数组在一瞬间被扩容和缩容。
        我们先看下扩容前的遍历过程

        假如第8次迭代后,数组瞬间扩容,这个时候遍历过程是

        此时多了一次对下标为15的遍历,可以想象这次遍历应该会重复下标为15%8=7遍历(即第8次)的元素。所以dictScan具有潜在对一个元素遍历多次的问题。我们再看第7次迭代时发生瞬间扩容的情况

        此时数组下标为11的遍历(即第8次遍历)会部分重复下标为3的遍历(即第7次遍历)元素。而之后的遍历就不会重复了。
        我们再看下数组的缩容。为缩容前的状态是

        如果第16次遍历时突然缩容,则遍历过程是

        可见第16次遍历的是新数组下标为7的元素,和第15次遍历老数组下标为7的元素不同,本次遍历的结果包含前者(因为它还包含之前下标为15的元素)。所以也存在元素重复遍历的问题。
        我们看下第15次遍历时突然缩容的遍历过程

        因为缩容到8,所以最后一次遍历下标7的情况,既包括之前老数组下标为7的元素,也包含老数组下标为15的元素。所以本次遍历不会产生重复遍历元素的问题。
        我们再看下第14次遍历突然缩容的遍历过程

        第14次本来是要遍历下标为11的元素。由于发生缩容,就遍历新的数组的下标为3的元素。所以第14的遍历包含第13次的遍历元素。
        一个数组如此,像dict结构中有两个dictht的情况,则稍微复杂点。我们通过下图可以发现,不同时机ht[0]扩容或者缩容,都可以保证元素被全遍历

        上面测试的代码是:
[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. #define TWO_FOUR_MASK 15  
  2. #define TWO_THREE_MASK 7  
  3.   
  4. static unsigned long rev(unsigned long v) {  
  5.     unsigned long s = 8 * sizeof(v);  
  6.     unsigned long mask = ~0;  
  7.     while ((s >>= 1) > 0) {  
  8.         mask ^= (mask <<s);  
  9.         v = ((v >> s) & mask) | ((v << s) & ~mask);  
  10.     }  
  11.     return v;  
  12. }  
  13.   
  14. unsigned long loop_single_expand_shrinks(unsigned long v, int change, int expand) {  
  15.     unsigned long m0 = 0;  
  16.   
  17.     if (expand) {  
  18.         if (change) {  
  19.             m0 = TWO_FOUR_MASK;  
  20.         }  
  21.         else {  
  22.             m0 = TWO_THREE_MASK;  
  23.         }  
  24.     }  
  25.     else {  
  26.         if (change) {  
  27.             m0 = TWO_THREE_MASK;  
  28.         }  
  29.         else {  
  30.             m0 = TWO_FOUR_MASK;  
  31.         }  
  32.     }  
  33.   
  34.     unsigned long t0idx = t0idx = v & m0;   
  35.     printf(" t0Index: %lu ", t0idx);  
  36.   
  37.     v |= ~m0;  
  38.     v = rev(v);  
  39.     v++;  
  40.     v = rev(v);  
  41.     return v;  
  42. }  
  43.   
  44. unsigned long loop(unsigned long v) {  
  45.     unsigned long m0 = TWO_THREE_MASK;  
  46.     unsigned long m1 = TWO_FOUR_MASK;  
  47.   
  48.     unsigned long t0idx = v & m0;  
  49.     printf(" t0Index: %lu ", t0idx);  
  50.   
  51.     printf(" t1Index: ");  
  52.     do {  
  53.         unsigned long t1idx = v & m1;  
  54.         printf("%lu ", t1idx);  
  55.         v = (((v | m0) + 1) & ~ m0) | (v & m0);  
  56.     } while (v & (m0 ^ m1));  
  57.   
  58.     v |= ~m0;  
  59.     v = rev(v);  
  60.     v++;  
  61.     v = rev(v);  
  62.     return v;  
  63. }  
  64.   
  65. unsigned long loop_expand_shrinks(unsigned long v, int change, int expand) {  
  66.     unsigned long m0 = 0;  
  67.     unsigned long m1 = 0;  
  68.     if (!change) {  
  69.         m0 = TWO_THREE_MASK;  
  70.         m1 = TWO_FOUR_MASK;  
  71.   
  72.         unsigned long t0idx = v & m0;  
  73.         if (expand) {  
  74.             printf(" t0Index: %lu ", t0idx);  
  75.             printf(" t1Index: ");  
  76.         }  
  77.         else {  
  78.             printf(" t1Index: %lu ", t0idx);  
  79.             printf(" t0Index: ");  
  80.         }  
  81.           
  82.         do {  
  83.             unsigned long t1idx = v & m1;  
  84.             printf("%lu ", t1idx);  
  85.             v = (((v | m0) + 1) & ~ m0) | (v & m0);  
  86.         } while (v & (m0 ^ m1));  
  87.     }  
  88.     else {  
  89.         if (expand) {  
  90.             m0 = TWO_FOUR_MASK;  
  91.         }  
  92.         else {  
  93.             m0 = TWO_THREE_MASK;  
  94.         }  
  95.   
  96.         unsigned long t0idx = v & m0;  
  97.         printf(" t0Index: %lu ", t0idx);  
  98.     }  
  99.   
  100.     v |= ~m0;  
  101.     v = rev(v);  
  102.     v++;  
  103.     v = rev(v);  
  104.     return v;  
  105. }  
  106.   
  107. void print_binary(unsigned long v) {  
  108.     char s[128] = {0};  
  109.     _itoa_s(v, s, sizeof(s), 2);  
  110.     printf("0x%032s", s);  
  111. }  
  112.   
  113. void check_loop_normal() {  
  114.     unsigned long v = 0;  
  115.     do   
  116.     {  
  117.         print_binary(v);  
  118.         v = loop(v);  
  119.         printf("\n");  
  120.     } while (v != 0);  
  121. }  
  122.   
  123. void check_loop_expand_shrinks(int expand) {  
  124.     int loop_count = 9;  
  125.   
  126.     for (int n  = 0; n < loop_count; n++) {  
  127.         unsigned long v = 0;  
  128.         int change = 0;  
  129.         int call_count = 0;  
  130.         do   
  131.         {  
  132.             if (call_count == n) {  
  133.                 change = 1;  
  134.             }  
  135.             print_binary(v);  
  136.             v = loop_expand_shrinks(v, change, expand);  
  137.             call_count++;  
  138.             printf("\n");  
  139.         } while (v != 0);  
  140.         printf("\n");  
  141.     }  
  142. }  
  143.   
  144. void check_loop_single_expand_shrinks(int expand) {  
  145.     int loop_count = 17;  
  146.   
  147.     for (int n  = 0; n < loop_count; n++) {  
  148.         unsigned long v = 0;  
  149.         int change = 0;  
  150.         int call_count = 0;  
  151.         do   
  152.         {  
  153.             if (call_count == n) {  
  154.                 change = 1;  
  155.             }  
  156.             print_binary(v);  
  157.             v = loop_single_expand_shrinks(v, change, expand);  
  158.             call_count++;  
  159.             printf("\n");  
  160.         } while (v != 0);  
  161.         printf("\n");  
  162.     }  
  163. }  
0 0
原创粉丝点击