码迷,mamicode.com
首页 > 其他好文 > 详细

Redis Scan迭代器遍历操作原理

时间:2015-05-25 12:48:07      阅读:937      评论:0      收藏:0      [点我收藏+]

标签:

Redis在2.8.0版本新增了众望所归的scan操作,从此再也不用担心敲入了keys*, 然后举起双手看着键盘等待漫长的系统卡死了···

命令的官方介绍在这里, 中文版由huangz同学细心翻译了,作者Antirez的介绍在这里:Finally Redis collections are iterable (我又邪恶的想到了之前他那次机器down机的事故了···)。

具体的使用参考上面的链接即可,这里大概介绍一下Scan操作的实现原理。

Redis的SCAN操作由于其整体的数据设计,无法提供特别准的scan操作,仅仅是一个“can ‘ t guarantee , just do my best”的实现,优缺点如下:

  • 优点:
    • 提供键空间的遍历操作,支持游标,复杂度O(1), 整体遍历一遍只需要O(N);
    • 提供结果模式匹配;
    • 支持一次返回的数据条数设置,但仅仅是个hints,有时候返回的会多;
    • 弱状态,所有状态只需要客户端需要维护一个游标;
  • 缺点:
    • 无法提供完整的快照遍历,也就是中间如果有数据修改,可能有些涉及改动的数据遍历不到;
    • 每次返回的数据条数不一定,极度依赖内部实现;
    • 返回的数据可能有重复,应用层必须能够处理重入逻辑;

所以结论是Scan是一个不错的但也让人又爱又恨的命令···。下面来介绍一下代码。

首先scanCommand 函数处理简单的scan操作,其他类似hscan函数跟这个的区别就是hscan需要取获取一遍key对应的空间或者说域,他们主要都是嚼用了通用的scan操作函数:scanGenericCommand 。

scanGenericCommand 函数分4步:

第一步当然就是解析参数了,比如count, match匹配参数;

第二部是需要去做真正的扫描键 的操作了,redis为了性能考虑,对于小数据结构会转换为ziplist,intset数据结构因此需要区分这2类,对于后者,由于其本身比较小,因此可完全可以在这一次scan操作的时候返还所有的数据,反正不大的。

另外一类就是正常的hash表所代表的扫描了,其扫描路径比较复杂,好吧,我看了好几次都没有看明白这到底是怎么扫描的,这几天啃也要啃出来

  /* Handle the case of a hash table. */

  ht = NULL;

  if (o == NULL) {//键扫描

      ht = c->db->dict;

  } else if (o->type == REDIS_SET && o->encoding == REDIS_ENCODING_HT) {

      ht = o->ptr;

  } else if (o->type == REDIS_HASH && o->encoding == REDIS_ENCODING_HT) {

      ht = o->ptr;

      count *= 2; /* We return key / value for this type. */

  } else if (o->type == REDIS_ZSET && o->encoding == REDIS_ENCODING_SKIPLIST) {

      zset *zs = o->ptr;

      ht = zs->dict;

      count *= 2; /* We return key / value for this type. */

  }

由于redis的ziplist, intset等类型数据量挺少,所以可用一次返回的。下面的else if 做这个事情。全部返回一个key 。

  if (ht) {//一般的存储,不是intset, ziplist

      void *privdata[2];



      /* We pass two pointers to the callback: the list to which it will

       * add new elements, and the object containing the dictionary so that

       * it is possible to fetch more data in a type-dependent way. */

      privdata[0] = keys;

      privdata[1] = o;

      do {

          //一个个扫描,从cursor开始,然后调用回调函数将数据设置到keys返回数据集里面。

          cursor = dictScan(ht, cursor, scanCallback, privdata);

      } while (cursor && listLength(keys) < count);     } else if (o->type == REDIS_SET) {

      int pos = 0;

      int64_t ll;



      while(intsetGet(o->ptr,pos++,&ll))//将这个set里面的数据全部返回,因为它是压缩的intset,会很小的。

          listAddNodeTail(keys,createStringObjectFromLongLong(ll));

      cursor = 0;

  } else if (o->type == REDIS_HASH || o->type == REDIS_ZSET) {//那么一定是ziplist了,字符串表示的数据结构,不会太大。

      unsigned char *p = ziplistIndex(o->ptr,0);

      unsigned char *vstr;

      unsigned int vlen;

      long long vll;



      while(p) {//扫描整个键,然后全部返回这一条。并且返回cursor为0表示没东西了。其实这个就等于没有遍历

          ziplistGet(p,&vstr,&vlen,&vll);

          listAddNodeTail(keys,

               (vstr != NULL) ? createStringObject((char*)vstr,vlen) : createStringObjectFromLongLong(vll));

          p = ziplistNext(o->ptr,p);

      }

      cursor = 0;

  } else {

      redisPanic("Not handled encoding in SCAN.");

  }

上面简单的地方在于如果这个键是已REDIS_SET或者REDIS_HASH或者REDIS_ZSET行事存储的话,那么只需要扫描所有的键,然后一个个将其加入到临时的列表里面,以备返回给客户端。

最难的地方在于dictScan 函数,里面是各种位运算。

随后第三步就是进行结果的过滤了,一般就是用match参数代表的字符串去做匹配,看是否需要过滤数据。

第四步就是将收集到的数据返回给客户端。然后就完成了请求。

dictScan  原理:

好吧,我看了2次,没看懂·····先做饭··

已上转载至 http://chenzhenianqing.cn/articles/1090.html

续上一篇文章 Redis Scan迭代器遍历操作原理(一)–基础 ,这里着重讲一下dictScan函数的原理,其实也就是redis SCAN操作最有价值(也是最难懂的部分)。

关于这个算法的源头,来自于githup这里:Add SCAN command #579,长篇的讨论,确实难懂····建议看看这帖子,antirez 跟pietern 关于这个奇怪算法的讨论···

这个算法的作者是:Pieter Noordhuis,作者称其为:reverse binary iteration ,不知道我一对一翻译为“反向二进制迭代器”可不可以,不过any way ··作者自己也没有明确的证明其真假:

antirez: Hello @pietern! I’m starting to re-evaluate the idea of an iterator for Redis, and the first item in this task is definitely to understand better your pull request and implementation. I don’t understand exactly the implementation with the reversed bits counter…
I wonder if there is a way to make that more intuitive… so investing some more time into this, and if I fail I’ll just merge your code trying to augment it with more comments…
Hard to explain but awesome.
pietern: Although I don’t have a formal proof for these guarantees, I’m reasonably confident they hold. I worked through every hash table state (stable, grow, shrink) and it appears to work everywhere by means of the reverse binary iteration (for lack of a better word).

下面从零开始讲一下redis的迭代器应该怎么设计,以及为什么不这么设计,而要这么设计·····

0.可用性 保证(Guarantees):

1.迭代结果可以重复;

2.整个迭代过程中,没有变化(增加删除)过的key必须出现在结果中;

redis的key是用hash存在的,key分布在数组的槽位内,下标从0到2^N,并且采用链表解决冲突。

hash会自动扩容或者缩小,并且每次 都是按2^N变化的。具体可以参阅:Redis源码学习-Dict/hash 字典

1.最简单暴力的方法:顺序迭代:

这个简单,从0到2^N下标扫描一次,每次返回一个slot(槽位,也就是数组的一项,下同)或者多个slot的数据,这样实现非常简单,在不发生rehash的时候,这种方法没问题,能够完成前面的要求。,但有以下问题:
1.如果后来字典扩容了,比如2,4倍长度,那么能够保证一定能找出没变化的key,但是却会出现大量重复。

比如当前的key数组大小是8,后来变为16了,比如从0,1,2,3““顺序扫描,如果数组发生扩容,那么前面的0,1,2,3 slot里面的数据会发生一部分迁移到对应的8,9,10,11 slot里面去,并且这个量挺大;

2.如果字典缩小了,比如从16缩小到8, 原先scan已经遍历了0,1,2,3 ,然后发生缩小,这样后来迭代停止在7号slot,但是8,9,10,11这几个slot的数据会分别合并到0,1,2,3里面去,从而scan就没有扫描出这部分元素出来,无法保证可用性;

3.在发生rehashing的过程中,这个肯定有问题的。

2.中间的改进版本:

为了避免上面第一种方法中第1个问题,也就是大量重复的问题,我们可以改进为这样迭代扫描:如果字典大小为8, 那么扫描的时候,总是这么扫描:0,4,     1,5,     2,6,      3,7,  也就是访问完i 后,再访问i+2^(N-1), 这样如果已经访问过0,4, 1,5 了,当访问完2号slot之后,发生了扩容,变成了字典大小是6, 那么我们不需要再次去访问8,9号了,原因是8,9号里面的数据一定是从0和1里面迁移过去的。

但很可惜,这样还是无法解决字典缩小的时候没有访问问题,比如访问完0后,发生字典缩小,原来8号的数据迁移到了0号,然后按照算法,会去访问4号的。这样就会有问题。

2.redis的反向二进制位迭代器 原理:

首先从直观感觉上,跟第二种方法类似的跳跃扫描,但是redis的方法更加完善。下面一步步的来介绍一下redis的SCAN原理

首先我们知道,这个迭代操作有下面几个地方需要注意:

  1. 字典大小不变的时候;
  2. 字典大小扩容的时候 ;
  3. 字典大小缩小的时候;
  4. 发生rehash的时候;

对于最简单的时候,也就是没有发生字典大小变化,那么最简单了,按照redis现在的方式处理如下,然后再扩展到redis怎么处理变化的时候。

先贴一下代码:

  

Redis Scan迭代器遍历操作原理

标签:

原文地址:http://www.cnblogs.com/thrillerz/p/4527478.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!