当前位置:   article > 正文

Redis命令之scan、sscan、hscan、zcan

hscan

官方文档:https://redis.io/commands/scan#the-count-option
与Keys、smembers的比较

  • keys命令以遍历的方式迭代整个库,实现的复杂度是 O(n),库中的key越多,速度越慢,由于Redis的单线程处理,阻塞的时间也就越长。smembers也一样,只不过它并不是阻塞整个库,影响只针对单个set,当set过大时会存在同样的问题。
  • scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程。
  • scan命令提供了limit参数,可以控制每次返回结果的最大条数。
  • scan命令返回的结果有可能重复,因此需要客户端去重

SCAN语法

 

  1. scan cursor [MATCH pattern] [COUNT count] #基于整个redis库进行扫描
  2. sscan key cursor [MATCH pattern] [COUNT count] #扫描指定的set类型的key
  3. hscan key cursor [MATCH pattern] [COUNT count] #扫描指定的hash类型的key
  4. zscan key cursor [MATCH pattern] [COUNT count] #扫描指定的zset类型的key

  参数说明:

  • cursor表示本次扫描的开始游标,可以理解成从开始索引。
  • pattern表示正则表达式
  • count表示希望返回的个数,实际返回个数会围绕该数波动,返回的个数可能等于该值也可能有一定出入,默认为10。

SCAN返回值
        scan返回值
是两个值的数组:第一个值表示在下一个调用中使用的新游标,第二个值是本次迭代结果集。如果已经完成了一次完整的迭代,那么服务器会返回一个为0的游标,告诉客户端,Redis已经对库<或set/zset、hash>完成了一次完整的迭代。

  1. 192.168.192.128:6703> scan 0 MATCH user.* COUNT 5
  2. 1) "0"
  3. 2) 1) "user.stz"
  1. 192.168.192.128:6703> sadd setkey a01 a02 a03 a04 a05 a05 b02 b03 b04 b05
  2. -> Redirected to slot [2440] located at 192.168.192.128:6701
  3. (integer) 9
  4. 192.168.192.128:6701> sscan setkey 0 MATCH a* COUNT 2
  5. 1) "6"
  6. 2) 1) "a03"
  7. 192.168.192.128:6701> sscan setkey 6 MATCH a* COUNT 2
  8. 1) "1"
  9. 2) 1) "a02"
  10. 192.168.192.128:6701> sscan setkey 1 MATCH a* COUNT 2
  11. 1) "5"
  12. 2) 1) "a04"
  13. 2) "a05"
  14. 192.168.192.128:6701> sscan setkey 5 MATCH a* COUNT 2
  15. 1) "0"
  16. 2) 1) "a01"
  17. 192.168.192.128:6701>

以下内容摘自:https://www.jianshu.com/p/be15dc89a3e8
SCAN的遍历顺序
    
关于scan命令的遍历顺序,我们可以用一个小栗子来具体看一下。

  1. 127.0.0.1:6379> keys *
  2. 1) "db_number"
  3. 2) "key1"
  4. 3) "myKey"
  5. 127.0.0.1:6379> scan 0 MATCH * COUNT 1
  6. 1) "2"
  7. 2) 1) "db_number"
  8. 127.0.0.1:6379> scan 2 MATCH * COUNT 1
  9. 1) "1"
  10. 2) 1) "myKey"
  11. 127.0.0.1:6379> scan 1 MATCH * COUNT 1
  12. 1) "3"
  13. 2) 1) "key1"
  14. 127.0.0.1:6379> scan 3 MATCH * COUNT 1
  15. 1) "0"
  16. 2) (empty list or set)

我们的Redis中有3个key,我们每次只遍历一个一维数组中的元素。如上所示,SCAN命令的遍历顺序是
0->2->1->3,
这个顺序看起来有些奇怪。我们把它转换成二进制就好理解一些了。
00->10->01->11

我们发现每次这个序列是高位加1的。普通二进制的加法,是从右往左相加、进位。而这个序列是从左往右相加、进位的。这一点我们在redis的源码中也得到印证。

在dict.c文件的dictScan函数中对游标进行了如下处理

  1. v = rev(v);
  2. v++;
  3. v = rev(v);

意思是,将游标倒置,加一后,再倒置,也就是我们所说的“高位加1”的操作。

这里大家可能会有疑问了,为什么要使用这样的顺序进行遍历,而不是用正常的0、1、2……这样的顺序呢,这是因为需要考虑遍历时发生字典扩容与缩容的情况(不得不佩服开发者考虑问题的全面性)。

我们来看一下在SCAN遍历过程中,发生扩容时,遍历会如何进行。加入我们原始的数组有4个元素,也就是索引有两位,这时需要把它扩充成3位,并进行rehash。

rehash

原来挂接在xx下的所有元素被分配到0xx和1xx下。在上图中,当我们即将遍历10时,dict进行了rehash,这时,scan命令会从010开始遍历,而000和100(原00下挂接的元素)不会再被重复遍历。

再来看看缩容的情况。假设dict从3位缩容到2位,当即将遍历110时,dict发生了缩容,这时scan会遍历10。这时010下挂接的元素会被重复遍历,但010之前的元素都不会被重复遍历了。所以,缩容时还是可能会有些重复元素出现的。

Redis的rehash

rehash是一个比较复杂的过程,为了不阻塞Redis的进程,它采用了一种渐进式的rehash的机制。

  1. /* 字典 */
  2. typedef struct dict {
  3. // 类型特定函数
  4. dictType *type;
  5. // 私有数据
  6. void *privdata;
  7. // 哈希表
  8. dictht ht[2];
  9. // rehash 索引
  10. // 当 rehash 不在进行时,值为 -1
  11. int rehashidx; /* rehashing not in progress if rehashidx == -1 */
  12. // 目前正在运行的安全迭代器的数量
  13. int iterators; /* number of iterators currently running */
  14. } dict;

在Redis的字典结构中,有两个hash表,一个新表,一个旧表。在rehash的过程中,redis将旧表中的元素逐步迁移到新表中,接下来我们看一下dict的rehash操作的源码。

  1. /* Performs N steps of incremental rehashing. Returns 1 if there are still
  2. * keys to move from the old to the new hash table, otherwise 0 is returned.
  3. *
  4. * Note that a rehashing step consists in moving a bucket (that may have more
  5. * than one key as we use chaining) from the old to the new hash table, however
  6. * since part of the hash table may be composed of empty spaces, it is not
  7. * guaranteed that this function will rehash even a single bucket, since it
  8. * will visit at max N*10 empty buckets in total, otherwise the amount of
  9. * work it does would be unbound and the function may block for a long time. */
  10. int dictRehash(dict *d, int n) {
  11. int empty_visits = n*10; /* Max number of empty buckets to visit. */
  12. if (!dictIsRehashing(d)) return 0;
  13. while(n-- && d->ht[0].used != 0) {
  14. dictEntry *de, *nextde;
  15. /* Note that rehashidx can't overflow as we are sure there are more
  16. * elements because ht[0].used != 0 */
  17. assert(d->ht[0].size > (unsigned long)d->rehashidx);
  18. while(d->ht[0].table[d->rehashidx] == NULL) {
  19. d->rehashidx++;
  20. if (--empty_visits == 0) return 1;
  21. }
  22. de = d->ht[0].table[d->rehashidx];
  23. /* Move all the keys in this bucket from the old to the new hash HT */
  24. while(de) {
  25. uint64_t h;
  26. nextde = de->next;
  27. /* Get the index in the new hash table */
  28. h = dictHashKey(d, de->key) & d->ht[1].sizemask;
  29. de->next = d->ht[1].table[h];
  30. d->ht[1].table[h] = de;
  31. d->ht[0].used--;
  32. d->ht[1].used++;
  33. de = nextde;
  34. }
  35. d->ht[0].table[d->rehashidx] = NULL;
  36. d->rehashidx++;
  37. }
  38. /* Check if we already rehashed the whole table... */
  39. if (d->ht[0].used == 0) {
  40. zfree(d->ht[0].table);
  41. d->ht[0] = d->ht[1];
  42. _dictReset(&d->ht[1]);
  43. d->rehashidx = -1;
  44. return 0;
  45. }
  46. /* More to rehash... */
  47. return 1;
  48. }

通过注释我们就能了解到,rehash的过程是以bucket为基本单位进行迁移的。所谓的bucket其实就是我们前面所提到的一维数组的元素。每次迁移一个列表。下面来解释一下这段代码。

  • 首先判断一下是否在进行rehash,如果是,则继续进行;否则直接返回。
  • 接着就是分n步开始进行渐进式rehash。同时还判断是否还有剩余元素,以保证安全性。
  • 在进行rehash之前,首先判断要迁移的bucket是否越界。
  • 然后跳过空的bucket,这里有一个empty_visits变量,表示最大可访问的空bucket的数量,这一变量主要是为了保证不过多的阻塞Redis。
  • 接下来就是元素的迁移,将当前bucket的全部元素进行rehash,并且更新两张表中元素的数量。
  • 每次迁移完一个bucket,需要将旧表中的bucket指向NULL。
  • 最后判断一下是否全部迁移完成,如果是,则收回空间,重置rehash索引,否则告诉调用方,仍有数据未迁移。

由于Redis使用的是渐进式rehash机制,因此,scan命令在需要同时扫描新表和旧表,将结果返回客户端。

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/658615
推荐阅读
相关标签
  

闽ICP备14008679号