Redis之Scan

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Redis之Scan

场景

在平时线上Redis维护工作中,有事需要从Redis实例中成千上万个key中找出特定前缀的key列表来手动维护数据,Redis提供了一个keys的命令列出满足特定正则字符串规则的key。

keys有两个缺点,一是无法做返回值限制,一次性会返回所有满足条件的key,万一实例中有几百万个key满足条件,就麻烦了,

二是keys是遍历算法,复杂度是O(n),如果有实例中的key非常的多,这个指令会导致redis卡顿,所有其他读写redis的指令都会被延后甚至超时报错,

因为redis是单线程程序,顺序执行所有指令,其他指令必须等到当前的keys指令执行完了才能继续执行。

127.0.0.1:6379> keys *

1) "codehole3"

2) "codehole2"

3) "codehole1"

4) "company"

127.0.0.1:6379> keys codehole*

1) "codehole3"

2) "codehole2"

3) "codehole1"

redis在2.8版本中加入了scan指令,解决了keys的缺点,有如下特点:

  1. 复杂度虽然也是O(n),但它是通过游标分布进行的,不会阻塞线程。
  2. 提供limit参数,可以控制每次返回的最大结果数,limit只是一个hint,返回的结果可多可少。
  3. 通keys一样,提供模式匹配功能。
  4. 服务器不需要为游标保存状态,游标的唯一状态是scan返回给客户端的游标整数。
  5. 返回的结果可能会重复,需要客户端去重。
  6. 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的。
  7. 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为0。

scan基本用法

首先在redis中插入 10000 条数据做测试。

image.png

我们通过scan查找key99开头的key列表,scan提供了三个参数,第一个是cursor整数值,第二个是key的正则模式,第三个是遍历的limit hint,

第一次遍历时,cursor的值为0,然后将返回结果中第一个整数值作为下一次遍历的cursor,一直遍历到返回的cursor值为0时结束。

下面是遍历结果,使用....代替太多的输出。

127.0.0.1:6379> scan 0 match key99* count 1000

1) "15832"

2)  1) "key9937"

   ........

127.0.0.1:6379> scan 15832 match key99* count 1000

1) "12396"

2)  1) "key993"
   ........

127.0.0.1:6379> scan 12396 match key99* count 1000

1) "6154"

2)  1) "key9934"

   ........

127.0.0.1:6379> scan 6154 match key99* count 1000

1) "11286"

2)  1) "key9986"

    ........

127.0.0.1:6379> scan 6529 match key99* count 1000

1) "13657"

2)  1) "key9975"

    ........

127.0.0.1:6379> scan 14797 match key99* count 1000

1) "7379"

2) 1) "key9905"

   ........

127.0.0.1:6379> scan 7379 match key99* count 1000

1) "8999"

2) 1) "key9984"

   ........

127.0.0.1:6379> scan 8999 match key99* count 1000

1) "12799"

2) 1) "key9971"

   2) "key9926"

   3) "key9968"
    ......

127.0.0.1:6379> scan 12799 match key99* count 1000

1) "0"

2) (empty array)

从上面的过程中可以看出,虽然提供的limit是1000,但是返回的结果只有10个左右,因为这个limit不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量(约等于),如果将limit设置为10,你会发现返回结果是空的,但是游标值不为0,意味着遍历还没有结束。

127.0.0.1:6379> scan 0 match key99* count 10

1) "2560"

2) (empty array)

字典的结构

在redis中所有的key都存储在一个很大的字典中,这个字典的结构和java中的HashMap一样,是一个一维数组,二维链表的结构,

第一维数组的大小总是2的n次方(n>=0),扩容一次数组,大小空间加倍,也就是2的n次方+1。

scan指令返回的游标就是一维数组的位置索引,我们称这个位置索引为槽(slot),如果不考虑字典的扩容缩容,直接按数组下标挨个遍历即可,

limit参数表示需要遍历的槽位数,之所以返回的结果或多或少,因为不是所有槽位上都会存在链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能有多个,每次遍历都会将limit数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端。

scan遍历顺序

scan的遍历顺序非常特别,它并不是从一维数组的第0位一直遍历到末尾,而是采用了高位进位加法来遍历,用这种方式是为了在字典拓容和缩容时避免槽位的遍历重复和遗漏。

下图是普通加法和高位进位加法的区别。

image.png

字典拓容 rehash

当 LoadFactor 达到阈值时,需要重新分配一个新的2倍大小的数组,然后所有的元素将被重新rehash挂到新的数组下面,

rehash就是将元素的hash值对数组长度进行取模运算,这时每个元素挂接的槽位可能会发生变化,因为数组的长度是2的n次方,所以取模运算等价于位与操作。

下面的7、15、31称为字典的mask值,mask的作用就是保留hash值的低位,高位都被设置为0。

a mod 8  = a & (8 - 1) = a & 7
a mod 16 = a & (16 - 1) = a & 15
a mod 32 = a & (32 - 1) = a & 31

如下图,假设一个字典的长度由8位扩容到16位,3号槽位011将被rehash到3号槽位和11号槽位,即部分元素留在3号槽位,部分移动到11号槽位,

11的二进制是1011,即对3的二进制011增加了一个高位1,变成了1011,

也就是说,如果一个槽位的二进制数是xxx,数组长度由8位拓容到16位,rehash后的槽位将是 0xxx 和 1xxx (xxx + 8),

由16位拓容到32位时,二进制xxxx的元素将被rehash到 0xxxx 和 1xxxx(xxxx + 16)中,你会发现很有规律的是,rehash后的元素所在槽位,都是原有槽位的二进制数在高位加了一个0和一个1的两个槽位里。

image.png

拓容前后的遍历顺序

如下图所示,使用高位进位加法的遍历方式,会使的我们的槽位在rehash之后遍历起来也是相邻的,即原本是在同一个槽位的元素,在rehash后,依然是可以连续的进行遍历到的。

image.png

假如要遍历110(橙色)这个位置的链表,此时拓容了,拓容后,原本110里的元素被放到了0110和1110这两个元素里了(你会发现拓容后的槽位就是上面说的在高位加一个0和1),

此时我们可以直接从0110这里进行遍历,随后遍历到1110,这样即将原本在一个槽位里的元素也顺序紧凑的遍历掉了,0110前面的槽位也都是遍历过的,同时避免了对已经遍历过的槽位进行重复遍历的问题,为什么这么说呢?

如果我们不按照高位进位加法的方式进行遍历,而是按照数组的下标进行遍历,从0-7进行遍历,此时遍历到3这个下标了,发生了拓容,之前 0 1 2下标里的我们已经遍历过了,由于发生了拓容,0 1 2拓容后的部分元素又被放到了 8 9 10中,

但其实他们已经被我们扫描过了,此时我们继续从3开始往后遍历,遍历到了8 9 10的时候,则有扫了一遍之前已经扫过的元素,这就是为啥用高位进位加法的顺序了,

缩容也是,如果下标是0-15,扫描到3的时候,此时进行缩容,只剩下0-7了,之前在8 9 10的元素被缩到了0 1 2里面去,这时候按照下标顺序的话,就无法扫到8 9 10了,导致漏扫的情况,

由于缩容后例如10这个槽位,是010和110上两个链表的元素结合,所以可能会导致010已经scan过的元素再次重新被scan一次,就出现了返回重复结果的情况。

渐进式hash

Java的HashMap在拓容时,会一次性将旧数组下的全部元素转移到新数组中,如果HashMap中的元素特别多,线程就会出现卡顿的情况,redis为了解决卡顿的问题,使用了渐进式rehash,

渐进式rehash就是,同时保留旧数组和新数组,在定时任务中以及后续对hash的指令操作中渐渐的将旧数组中挂接的元素迁移到新数组上,处于rehash中的字典,需要同时访问新旧两个数组结构,如果在旧数组中找不到,就去新数组中查找,

scan对于rehash中的字典,需要同时扫描新旧槽位,然后将结果整合返回给客户端。

集合的scan指令

scan除了可以遍历redis中的所有key,也可以对指定的容器集合进行遍历,

对于 zset 使用 zscan,对于 hash 使用 hscan,对于 set 使用 sscan,原理与scan类似,因为hash底层是字典,set也是特殊的hash,zset内部也使用了字典存储所有元素内容。

大key扫描

如果不慎在redis当中形成了一个很大的对象,例如一个很大的hash或很大的zset,这对redis会有一些影响,

例如集群环境下,某个key太大,导致数据迁移卡顿,在内存分配的情况下,key太大,拓容时,将一次性申请更大的内存,也会导致redis卡顿,如果这个key被删除,内存会被一次性回收,又会导致卡顿,

在业务开发中,尽量减少大可以的产生,如果发现redis的内存大起大落,可能是大key导致的,定位大key也可以用scan进行扫描,

对于扫描出来的key,使用type获得key的类型,然后使用相应的数据结构的size或者len方法得到key的大小,对于每种类型,将大小排名的前若干名作为扫描结果展示出来,这种方式需要编写脚本,比较繁琐,redis直接提供了大key扫描功能

# 这个命令可能会导致 redis 的 ops大幅度提升

redis-cli -h 127.0.0.1 -p 7001 --bigkeys

# 使用 -i 0.1, 让指令每隔100条scan指令就休眠0.1s,ops将不会大幅度提升,但是扫描时间将会变长

redis-cli -h 127.0.0.1 -p 7001 --bigkeys -i 0.1
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
2月前
|
NoSQL Redis 数据库
12)Redis 的游标迭代器(scan)
12)Redis 的游标迭代器(scan)
33 1
|
缓存 NoSQL 算法
Redis之MoreKey问题及Scan命令解读
Redis之MoreKey问题及Scan命令解读
|
存储 NoSQL 算法
Redis进阶-如何从海量的 key 中找出特定的key列表 & Scan详解
Redis进阶-如何从海量的 key 中找出特定的key列表 & Scan详解
650 0
|
NoSQL Redis 数据安全/隐私保护
【Redis 技术探索】「数据迁移实战」手把手教你如何实现在线 + 离线模式进行迁移 Redis 数据实战指南(scan模式迁移)
【Redis 技术探索】「数据迁移实战」手把手教你如何实现在线 + 离线模式进行迁移 Redis 数据实战指南(scan模式迁移)
349 0
【Redis 技术探索】「数据迁移实战」手把手教你如何实现在线 + 离线模式进行迁移 Redis 数据实战指南(scan模式迁移)
|
存储 NoSQL 算法
【Redis核心原理专题】(1)「技术提升系列」分析探究如何实现LFU的热点key发现机制以及内部的Scan扫描技术的原理
【Redis核心原理专题】(1)「技术提升系列」分析探究如何实现LFU的热点key发现机制以及内部的Scan扫描技术的原理
197 0
【Redis核心原理专题】(1)「技术提升系列」分析探究如何实现LFU的热点key发现机制以及内部的Scan扫描技术的原理
|
NoSQL Java 程序员
Redis scan 命令的一次坑
Redis scan 命令的一次坑
345 0
|
NoSQL Redis
Redis的scan命令学习
Redis的scan命令学习
231 0
|
NoSQL Shell Redis
Redis 中 scan 命令太坑了,千万别乱用!!
原本以为自己对redis命令还蛮熟悉的,各种数据模型各种基于redis的骚操作。但是最近在使用redis的scan的命令式却踩了一个坑,顿时发觉自己原来对redis的游标理解的很有限。所以记录下这个踩坑的过程,背景如下:
3526 1
Redis 中 scan 命令太坑了,千万别乱用!!
|
NoSQL Java Linux
使用redis的scan指令详解
使用redis的scan指令详解
583 0
|
NoSQL Java 数据库
Redis命令:scan实现模糊查询
Redis命令:scan实现模糊查询
515 0