详解Redis SCAN命令实现有限保证的原理

SCAN命令可以为用户保证:从完整遍历开始直到完整遍历结束期间,一直存在于数据集内的所有元素都会被完整遍历返回,但是同一个元素可能会被返回多次。如果一个元素是在迭代过程中被添加到数据集的,又或者是在迭代过程中从数据集中被删除的,那么这个元素可能会被返回,也可能不会返回。

这是如何实现的呢,先从Redis中的字典dict开始。Redis的数据库是使用dict作为底层实现的。

字典数据类型

Redis中的字典由dict.h/dict结构表示:

typedef struct dict {
 dictType *type;
 void *privdata;
 dictht ht[2];
 long rehashidx; /* rehashing not in progress if rehashidx == -1 */
 unsigned long iterators; /* number of iterators currently running */
} dict;

typedef struct dictht {
 dictEntry **table;
 unsigned long size;
 unsigned long sizemask;
 unsigned long used;
} dictht;

字典由两个哈希表dictht构成,主要用做rehash,平常主要使用ht[0]哈希表。

哈希表由一个成员为dictEntry的数组构成,size属性记录了数组的大小,used属性记录了已有节点的数量,sizemask属性的值等于size - 1。数组大小一般是2n,所以sizemask二进制是0b11111...,主要用作掩码,和哈希值一起决定key应该放在数组的哪个位置。

求key在数组中的索引的计算方法如下:

index = hash & d->ht[table].sizemask;

也就是根据掩码求低位值。

rehash的问题

字典rehash时会使用两个哈希表,首先为ht[1]分配空间,如果是扩展操作,ht[1]的大小为第一个大于等于2倍ht[0].used的2n,如果是收缩操作,ht[1]的大小为第一个大于等于ht[0].used的2n。然后将ht[0]的所有键值对rehash到ht[1]中,最后释放ht[0],将ht[1]设置为ht[0],新创建一个空白哈希表当做ht[1]。rehash不是一次完成的,而是分多次、渐进式地完成。

举个例子,现在将一个size为4的哈希表ht[0](sizemask为11, index = hash & 0b11)rehash至一个size为8的哈希表ht[1](sizemask为111, index = hash & 0b111)。

ht[0]中处于bucket0位置的key的哈希值低两位为00,那么rehash至ht[1]时index取低三位可能为000(0)和100(4)。也就是ht[0]中bucket0中的元素rehash之后分散于ht[1]的bucket0与bucket4,以此类推,对应关系为:

 ht[0] -> ht[1]
 ----------------
  0 -> 0,4
  1 -> 1,5
  2 -> 2,6
  3 -> 3,7

如果SCAN命令采取0->1->2->3的顺序进行遍历,就会出现如下问题:

•扩展操作中,如果返回游标1时正在进行rehash,ht[0]中的bucket0中的部分数据可能已经rehash到ht[1]中的bucket[0]或者bucket[4],在ht[1]中从bucket1开始遍历,遍历至bucket4时,其中的元素已经在ht[0]中的bucket0中遍历过,这就产生了重复问题。
•缩小操作中,当返回游标5,但缩小后哈希表的size只有4,如何重置游标?

SCAN的遍历顺序

SCAN命令的遍历顺序,可以举一个例子看一下:

127.0.0.1:6379[3]> keys *
1) "bar"
2) "qux"
3) "baz"
4) "foo"
127.0.0.1:6379[3]> scan 0 count 1
1) "2"
2) 1) "bar"
127.0.0.1:6379[3]> scan 2 count 1
1) "1"
2) 1) "foo"
127.0.0.1:6379[3]> scan 1 count 1
1) "3"
2) 1) "qux"
 2) "baz"
127.0.0.1:6379[3]> scan 3 count 1
1) "0"
2) (empty list or set)

可以看出顺序是0->2->1->3,很难看出规律,转换成二进制观察一下:

00 -> 10 -> 01 -> 11

二进制就很明了了,遍历采用的顺序也是加法,但每次是高位加1的,也就是从左往右相加、从高到低进位的。

SCAN源码

SCAN遍历字典的源码在dict.c/dictScan,分两种情况,字典不在进行rehash或者正在进行rehash。

不在进行rehash时,游标是这样计算的:

m0 = t0->sizemask;
// 将游标的umask位的bit都置为1
v |= ~m0;
// 反转游标
v = rev(v);
// 反转后+1,达到高位加1的效果
v++;
// 再次反转复位
v = rev(v);

当size为4时,sizemask为3(00000011),游标计算过程:

   v |= ~m0 v = rev(v) v++  v = rev(v)
00000000(0) -> 11111100 -> 00111111 -> 01000000 -> 00000010(2)
00000010(2) -> 11111110 -> 01111111 -> 10000000 -> 00000001(1)
00000001(1) -> 11111101 -> 10111111 -> 11000000 -> 00000011(3)
00000011(3) -> 11111111 -> 11111111 -> 00000000 -> 00000000(0)

遍历size为4时的游标状态转移为0->2->1->3。

同理,size为8时的游标状态转移为0->4->2->6->1->5->3->7,也就是000->100->010->110->001->101->011->111。

再结合前面的rehash:

  ht[0] -> ht[1]
  ----------------
   0  ->  0,4
   1  ->  1,5
   2  ->  2,6
   3  ->  3,7

可以看出,当size由小变大时,所有原来的游标都能在大的哈希表中找到相应的位置,并且顺序一致,不会重复读取并且不会遗漏。

当size由大变小的情况,假设size由8变为了4,分两种情况,一种是游标为0,2,1,3中的一种,此时继续读取,也不会遗漏和重复。

但如果游标返回的不是这四种,例如返回了7,7&11之后变为了3,所以会从size为4的哈希表的bucket3开始继续遍历,而bucket3包含了size为8的哈希表中的bucket3与bucket7,所以会造成重复读取size为8的哈希表中的bucket3的情况。

所以,redis里rehash从小到大时,SCAN命令不会重复也不会遗漏。而从大到小时,有可能会造成重复但不会遗漏。

当正在进行rehash时,游标计算过程:

  /* 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) {
      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) {
        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 = rev(v);
      /* Continue while bits covered by mask difference is non-zero */
    } while (v & (m0 ^ m1));

算法会保证t0是较小的哈希表,不是的话t0与t1互换,先遍历t0中游标所在的bucket,然后再遍历较大的t1。

求下一个游标的过程基本相同,只是把m0换成了rehash之后的哈希表的m1,同时还加了一个判断条件:

v & (m0 ^ m1)

size4的m0为00000011,size8的m1为00000111,m0 ^ m1取值为00000100,即取二者mask的不同位,看游标在这些标志位是否为1。

假设游标返回了2,并且正在进行rehash,此时size由4变成了8,二者mask的不同位是低第三位。

首先遍历t0中的bucket2,然后遍历t1中的bucket2,公式计算出的下一个游标为6(00000110),低第三位为1,继续循环,遍历t1中的bucket6,然后计算游标为1,结束循环。

所以正在rehash时,是两个哈希表都遍历的,以避免遗漏的情况。

总结

以上所述是小编给大家介绍的Redis SCAN命令实现有限保证的原理,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

(0)

相关推荐

  • redis 用scan指令 代替keys指令(详解)

    众所周知,当redis中key数量越大,keys 命令执行越慢,而且最重要的会阻塞服务器,对单线程的redis来说,简直是灾难,终于找到了替代命令scan. SCAN cursor [MATCH pattern] [COUNT count] SCAN 命令及其相关的 SSCAN 命令. HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements): SCAN 命令用于迭代当前数据库中的数据库键. S

  • Redis中Scan命令的基本使用教程

    前言 Redis中有一个经典的问题,在巨大的数据量的情况下,做类似于查找符合某种规则的Key的信息,这里就有两种方式, 一是keys命令,简单粗暴,由于Redis单线程这一特性,keys命令是以阻塞的方式执行的,keys是以遍历的方式实现的复杂度是 O(n),Redis库中的key越多,查找实现代价越大,产生的阻塞时间越长. 二是scan命令,以非阻塞的方式实现key值的查找,绝大多数情况下是可以替代keys命令的,可选性更强 以下写入100000条key***:value***格式的测试数据(

  • Redis中scan命令的深入讲解

    前言 熟悉Redis的人都知道,它是单线程的.因此在使用一些时间复杂度为O(N)的命令时要非常谨慎.可能一不小心就会阻塞进程,导致Redis出现卡顿. 有时,我们需要针对符合条件的一部分命令进行操作,比如删除以test_开头的key.那么怎么获取到这些key呢?在Redis2.8版本之前,我们可以使用keys命令按照正则匹配得到我们需要的key.但是这个命令有两个缺点: 没有limit,我们只能一次性获取所有符合条件的key,如果结果有上百万条,那么等待你的就是"无穷无尽"的字符串输出

  • php redis扩展支持scan命令实现方法

    在使用阿里云的kvstore的时候,刚开始是属于公测,不收费,后来要成商业模式,收费了,8块钱一小时,太贵了,于是想到了删除部分无用的数据,但是数据量过于庞大,又不是使用keys * 来匹配(使用keys * 会直接把你redis卡死的),后期了解到了scan可以游标的找到所有的keys,于是开始捣鼓(发现我好多废话).. 开干.. [codesyntax lang="python"] # git clone https://github.com/phpredis/phpredis #

  • 详解Redis SCAN命令实现有限保证的原理

    SCAN命令可以为用户保证:从完整遍历开始直到完整遍历结束期间,一直存在于数据集内的所有元素都会被完整遍历返回,但是同一个元素可能会被返回多次.如果一个元素是在迭代过程中被添加到数据集的,又或者是在迭代过程中从数据集中被删除的,那么这个元素可能会被返回,也可能不会返回. 这是如何实现的呢,先从Redis中的字典dict开始.Redis的数据库是使用dict作为底层实现的. 字典数据类型 Redis中的字典由dict.h/dict结构表示: typedef struct dict { dictTy

  • 详解redis脚本命令执行问题(redis.call)

    1.redis-cli命令行中执行: # 调用redis命令设置缓存 # 不传参数 eval "return redis.call('set', 'name1', 'Tom')" 0 # 传入1个值参数 eval "return redis.call('set', 'name2', ARGV[1])" 0 "Tom" # 传入1个键名参数和1个值参数 eval "return redis.call('set', KEYS[1], ARG

  • Redis SCAN命令详解

    目录 1. 获取指定前缀的key 需求描述: 解决方案: 2. SCAN命令 Redis Scan 命令用于迭代数据库中的数据库键. SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程. SCAN 返回一个包含两个元素的数组, 第一个元素是用于进行下一次迭代的新游标, 而第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素.如果新游标返回 0 表示迭代已结束. 相

  • 详解Redis命令和键_动力节点Java学院整理

    Redis命令用于在redis服务器上执行某些操作. 要在Redis服务器上运行的命令,需要一个Redis客户端. Redis客户端在Redis的包,这已经我们前面安装使用过了. 语法 Redis客户端的基本语法如下: $redis-cli 例子 下面举例说明如何使用Redis客户端. 要启动redis客户端,打开终端,输入命令Redis命令行:redis-cli.这将连接到本地服务器,现在就可以运行各种命令了. $redis-cli redis 127.0.0.1:6379> redis 12

  • 详解Redis瘦身指南

    Redis内存回收 Redis 服务器的最大占用内存量由配置项 maxmemory 决定,我们可以通过 config set maxmemory 2GB 的格式来配置.一旦 Redis 内存满,所有引起内存增加的操作都会被返回 error.作为专业 Redis 服务器我们通常将此项设置为0,以服务器系统内存来作为限制: 那么 Redis 使用内存达到了上限怎么办?Redis 为我们提供了几种选项以自动回收内存,可以通过配置项 maxmemory-policy 来配置: noeviction 不回

  • 详解redis中的锁以及使用场景

    分布式锁 什么是分布式锁? 分布式锁是控制分布式系统之间同步访问共享资源的一种方式. 为什么要使用分布式锁? ​ 为了保证共享资源的数据一致性. 什么场景下使用分布式锁? ​ 数据重要且要保证一致性 如何实现分布式锁? 主要介绍使用redis来实现分布式锁 redis事务 redis事务介绍: ​ 1.redis事务可以一次执行多个命令,本质是一组命令的集合. ​ 2.一个事务中的所有命令都会序列化,按顺序串行化的执行而不会被其他命令插入 ​ **作用:**一个队列中,一次性.顺序性.排他性的执

  • 详解Redis主从复制实践

    复制简介 Redis 作为一门非关系型数据库,其复制功能和关系型数据库(MySQL)来说,功能其实都是差不多,无外乎就是实现的原理不同.Redis 的复制功能也是相对于其他的内存性数据库(memcached)所具备特有的功能. Redis 复制功能主要的作用,是集群.分片功能实现的基础:同时也是 Redis 实现高可用的一种策略,例如解决单机并发问题.数据安全性等等问题. 服务介绍 在本文环境演示中,有一台主机,启动了两个 Redis 示例. 实现方式 Redis 复制实现方式分为下面三种方式:

  • 详解redis分布式锁的这些坑

    一.白话分布式 什么是分布式,用最简单的话来说,就是为了较低单个服务器的压力,将功能分布在不同的机器上面,本来一个程序员可以完成一个项目:需求->设计->编码->测试 但是项目多的时候,一个人也扛不住,这就需要不同的人进行分工合作了 这就是一个简单的分布式协同工作了: 二.分布式锁 首先看一个问题,如果说某个环节被终止或者别侵占,就会发生不可知的事情 这就会出现,设计好的或者设计的半成品会被破坏,导致后面环节出错: 这时候,我们就需要引入分布式锁的概念: 何为分布式锁 当在分布式模型下,

  • 详解Redis集群搭建的三种方式

    一.单节点实例 单节点实例还是比较简单的,平时做个测试,写个小程序如果需要用到缓存的话,启动一个 Redis 还是很轻松的,做为一个 key/value 数据库也是可以胜任的 二.主从模式(master/slaver) redis 主从模式配置 主从模式: redis 的主从模式,使用异步复制,slave 节点异步从 master 节点复制数据,master 节点提供读写服务,slave 节点只提供读服务(这个是默认配置,可以通过修改配置文件 slave-read-only 控制).master

  • 详解Redis复制原理

    前言 本文主要介绍Redis复制机制 一.配置与实践 配置 Redis实例分为主节点(master)和从节点(slave),默认情况下都是主节点.每一个从节点只能有一个主节点,但是每一个主节点可以有多个从节点(注意数量,多个从节点会导致主节点写命令多次发送从而过度消耗网络带宽,可用树状结构降低主节点负载).复制是单向的,只能从主节点复制到从节点.配置复制的方式由以下3种: 在redis-slave.conf配置文件中加入slaveof {masterHost} {masterPort} 在red

随机推荐