当前位置:   article > 正文

Redis的scan命令_redis-cli scan

redis-cli scan

最近研究redis大数据处理,网上查了查资料,这里记录一下。

 

在 redis 实际使用中,会遇到一个问题:如何从海量的 key 中找出满足特定前缀的 key 列表来?

1. 不要使用 keys*

redis 提供了一个简单暴力的指令 keys 用来列出所有满足特定正则字符串规则的 key。

keys xxx*

这个指令有致命的弊端,在实际环境中最好不要使用:

这个指令没有 offset、limit 参数,是要一次性吐出所有满足条件的 key,由于 redis 是单线程的,其所有操作都是原子的,而 keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,可能会引起缓存雪崩甚至数据库宕机。

我们可以通过配置设置禁用这些命令,在 redis.conf 中,在 SECURITY 这一项中,我们新增以下命令:

  1. rename-command flushall ""
  2. rename-command flushdb ""
  3. rename-command config ""
  4. rename-command keys ""

另外,对于FLUSHALL命令,需要设置配置文件中appendonly no,否则服务器是无法启动。

Redis 为了解决这个问题,它在 2.8 版本中加入了scan

scan 相比 keys 具备有以下特点:

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

scan 优缺点如下:
优点:

  • - 提供键空间的遍历操作,支持游标,复杂度O(1), 整体遍历一遍只需要O(N)
  • - 提供结果模式匹配
  • - 支持一次返回的数据条数设置,但仅仅是个hints,有时候返回的会多;
  • - 弱状态,所有状态只需要客户端需要维护一个游标;

缺点:

  • - 无法提供完整的快照遍历,也就是中间如果有数据修改,可能有些涉及改动的数据遍历不到
  • - 每次返回的数据条数不一定,极度依赖内部实现;
  • - 返回的数据可能有重复,应用层必须能够处理重入逻辑;

2. scan 使用

SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements):

  • SCAN 命令用于迭代当前数据库中的数据库键。
  • SSCAN 命令用于迭代集合键中的元素。
  • HSCAN 命令用于迭代哈希键中的键值对。
  • ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)

scan 参数提供了三个参数,第一个是 cursor 整数值,第二个是 key 的正则模式,第三个是遍历的 limit hint。第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。

3. Redis的字典的结构

在 Redis 中所有的 key 都存储在一个很大的字典中,这个字典的结构和 Java 中的 HashMap 一样,是一维数组 + 二维链表结构,第一维数组的大小总是 2^n(n>=0),扩容一次数组大小空间加倍,也就是 n++。

scan 指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽 (slot)。如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。limit 参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每一次遍历都会将 limit 数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端。

4. SCAN的遍历顺序

scan 的遍历顺序非常特别。它不是从第一维数组的第 0 位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏

首先我们用动画演示一下普通加法和高位进位加法的区别。

从动画中可以看出高位进位法从左边加,进位往右边移动,同普通加法正好相反。但是最终它们都会遍历所有的槽位并且没有重复。

关于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……这样的顺序呢,这是因为需要考虑遍历时发生字典扩容与缩容的情况(不得不佩服开发者考虑问题的全面性)。

4.1 扩容rehash

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

                                                                rehash

原来挂接在xx下的所有元素被分配到0xx和1xx下。

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

4.2 缩容rehash

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

4.3 渐进式 rehash

Java 的 HashMap 在扩容时会一次性将旧数组下挂接的元素全部转移到新数组下面。如果 HashMap 中元素特别多,rehash时线程就会出现卡顿现象。Redis 为了解决这个问题,它采用渐进式 rehash

它会同时保留旧数组和新数组,然后在定时任务中以及后续对 hash 的指令操作中渐渐地将旧数组中挂接的元素迁移到新数组上。这意味着要操作处于 rehash 中的字典,需要同时访问新旧两个数组结构。如果在旧数组下面找不到元素,还需要去新数组下面去寻找。

scan 也需要考虑这个问题,对与 rehash 中的字典,它需要同时扫描新旧槽位,然后将结果融合后返回给客户端。

5 大 key 扫描

有时候会因为业务人员使用不当,在 Redis 实例中会形成很大的对象,比如一个很大的 hash,一个很大的 zset 这都是经常出现的。这样的对象对 Redis 的集群数据迁移带来了很大的问题,因为在集群环境下,如果某个 key 太大,会数据导致迁移卡顿。另外在内存分配上,如果一个 key 太大,那么当它需要扩容时,会一次性申请更大的一块内存,这也会导致卡顿。如果这个大 key 被删除,内存会一次性回收,卡顿现象会再一次产生。

在平时的业务开发中,要尽量避免大 key 的产生

如果你观察到 Redis 的内存大起大落,这极有可能是因为大 key 导致的,这时候你就需要定位出具体是那个 key,进一步定位出具体的业务来源,然后再改进相关业务代码设计。

那如何定位大 key 呢?

为了避免对线上 Redis 带来卡顿,这就要用到 scan 指令,对于扫描出来的每一个 key,使用 type 指令获得 key 的类型,然后使用相应数据结构的 size 或者 len 方法来得到它的大小,对于每一种类型,保留大小的前 N 名作为扫描结果展示出来。

上面这样的过程需要编写脚本,比较繁琐,不过 Redis 官方已经在 redis-cli 指令中提供了这样的扫描功能,我们可以直接拿来即用。

  1. redis-cli -h 127.0.0.1 -p 7001 –-bigkeys

如果你担心这个指令会大幅抬升 Redis 的 ops 导致线上报警,还可以增加一个休眠参数。

  1. redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1

上面这个指令每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的时间会变长。

 

6. 通过xargs实现批量删除

6.1 第一种方式:

redis-cli -a "password" -n 0 -p 6379 keys "*:login" | xargs -i redis-cli -a "password" -n 0 -p 6379 del {}

  1. [root@localhost ~]# redis-cli -c -h 10.1.37.113 -p 7002 -a PASSWORD -n 0 KEYS hello*
  2. 1) "hello8"
  3. 2) "hello5"
  4. [root@localhost ~]# redis-cli -c -h 10.1.37.113 -p 7002 -a PASSWORD -n 0 KEYS "hello*" | xargs -i redis-cli -c -h 10.1.37.113 -p 7002 -a PASSWORD -n 0 DEL {}
  5. (integer) 1
  6. (integer) 1
  7. [root@localhost ~]# redis-cli -c -h 10.1.37.113 -p 7002 -a PASSWORD -n 0 KEYS hello*
  8. (empty list or set)

这样一个坏处每次都要建立一个连接,量小的话还可以接受,量大的话,效率不行。

6.2 第二种方式:

自从redis2.8以后就开始支持scan命令,模式匹配可以采取下面的形式来批删除大量的key

redis-cli -a "password" -n 0 -p 6379 --scan --pattern "*:login" | xargs -i redis-cli -a "password" -n 0 -p 6379 DEL {}

  1. [root@localhost ~]# redis-cli -c -h 10.1.37.113 -p 7002 -a PASSWORD -n 0 --scan --pattern "hello*"
  2. hello1
  3. hello4
  4. hello5
  5. [root@localhost ~]# redis-cli -c -h 10.1.37.113 -p 7002 -a PASSWORD -n 0 --scan --pattern "hello*" | xargs -i redis-cli -c -h 10.1.37.113 -p 7002 -a PASSWORD -n 0 DEL {}
  6. (integer) 1
  7. (integer) 1
  8. (integer) 1
  9. [root@localhost ~]# redis-cli -c -h 10.1.37.113 -p 7002 -a PASSWORD -n 0 --scan --pattern "hello*"
  10. [root@localhost ~]# redis-cli -c -h 10.1.37.113 -p 7002 -a PASSWORD -n 0 KEYS "hello*"
  11. (empty list or set)

速度处理也是非常快的。

6.3 第三种方式:

redis-cli -c -h $ip -p $port keys $pkey | xargs -r -t -n1 redis-cli -c -h $ip -p $port del

7. 批量删除kyes脚本

7.1 脚本造key测试

  1. #!/bin/bash
  2. redis_list=("10.1.37.113:7002")
  3. pkey_list=("ValuationRuleSummary" "ValuationRuleDetail" "MerchantValuation" "QueryValuationRules" "GetMerchantByUserId")
  4. for info in ${redis_list[@]}
  5. do
  6. echo "开始执行:$info"
  7. ip=`echo $info | cut -d \: -f 1`
  8. port=`echo $info | cut -d \: -f 2`
  9. for pkey in ${pkey_list[@]}
  10. do
  11. for i in `seq 0 10000`
  12. do
  13. echo $pkey"_"$i $i
  14. redis-cli -c -h $ip -p $port -a PASSWORD SET $pkey"_"$i $i
  15. done
  16. done
  17. done
  18. echo "完成"

7.2 第一种批量删除方式

  1. #!/bin/bash
  2. redis_list=("10.1.37.113:7000" "10.1.37.113:7001" "10.1.37.113:7002" "10.1.37.113:7003" "10.1.37.113:7004" "10.1.37.113:7005")
  3. pkey_list=("ValuationRuleSummary*" "ValuationRuleDetail*" "MerchantValuation*" "QueryValuationRules*" "GetMerchantByUserId*")
  4. for info in ${redis_list[@]}
  5. do
  6. echo "开始执行:$info"
  7. ip=`echo $info | cut -d \: -f 1`
  8. port=`echo $info | cut -d \: -f 2`
  9. for pkey in ${pkey_list[@]}
  10. do
  11. redis-cli -c -h $ip -p $port -a PASSWORD KEYS $pkey | xargs -n 1 -t -i redis-cli -c -h $ip -p $port -a PASSWORD DEL {}
  12. done
  13. done
  14. echo "完成"

7.3 通过keys文件列表操作

key.txt –待删除的key

  1. key1
  2. key2
  3. key3

redis_delete_key.sh
redis_list 填入集群节点

  1. redis_list=("127.0.0.1:6379" "127.0.0.1:6380")
  2. for info in ${redis_list[@]}
  3. do
  4. echo "开始执行:$info"
  5. ip=`echo $info | cut -d \: -f 1`
  6. port=`echo $info | cut -d \: -f 2`
  7. cat key.txt |xargs -t -n1 redis-cli -h $ip -p $port -c del
  8. done
  9. echo "完成"

7.4 删除集合shell

以集合set为例子:

  1. function delSet()
  2. {
  3.     key=$3
  4.     rs=`redis-cli -p $1 -a $2 exists $key`
  5.     scannum=100
  6.     #echo $rs
  7.     if [ `echo $rs | awk -v tem="0" '{print($1>tem)? "1":"0"}'` -eq "0" ] ; then
  8.         echo "redis doesn't exist the key $key"
  9.     else
  10.         cursor="1"
  11.         members=`redis-cli -p $1 -a $2 sscan $key 0 count $scannum`
  12.         while [ `echo $cursor | awk -v tem="0" '{print($1>tem)? "1":"0"}'` -eq "1" ] ; do
  13.             OLD_IFS="$IFS"
  14.             IFS=" "
  15.             array=($members)
  16.             IFS="$OLD_IFS"
  17.             count=1
  18.             delkeys=""
  19.             for var in ${array[@]}
  20.             do
  21.                 if [ $count -eq 1 ] ; then
  22.                     cursor=$var
  23.                     count=$((count+1))
  24.                 else
  25.                     #rs=`redis-cli -p $1 -a $2 SREM $key $var`
  26.                     #echo "del:$var"
  27.                     delkeys=$delkeys$var" "
  28.                 fi
  29.             done
  30.             rs=`redis-cli -p $1 -a $2 SREM $key $delkeys`
  31.             if [ `echo $cursor | awk -v tem="0" '{print($1>tem)? "1":"0"}'` -eq "1" ];then
  32.                 members=`redis-cli -p $1 -a $2 sscan $key $cursor count $scannum`
  33.             fi
  34.         done
  35.     fi
  36. }
  37. key="setkey"
  38. echo "start remove the key $key"
  39. delSet $1 $2 $key
  40. rs=`redis-cli -p $1 -a $2 del $key`
  41. echo "remove the key $key successfully"

7.5 redis-del-keys.sh

  1. #!/bin/bash
  2. ##redis主机IP
  3. host=$1
  4. ##redis端口
  5. port=$2
  6. ##key模式
  7. pattern=$3
  8. ##游标
  9. cursor=0
  10. ##退出信号
  11. signal=0
  12. ##循环获取key并删除
  13. while [ $signal -ne 1 ]
  14. do
  15. echo "cursor:${cursor}"
  16. sleep 2
  17. ##将redis scan得到的结果赋值到变量
  18. re=$(redis-cli -h $host -p $p -c scan $cursor count 1000 match $pattern)
  19. ##以换行作为分隔符
  20. IFS=$'\n'
  21. #echo $re
  22. echo 'arr=>'
  23. ##转成数组
  24. arr=($re)
  25. ##打印数组长度
  26. echo 'len:'${#arr[@]}
  27. ##第一个元素是游标值
  28. cursor=${arr[0]}
  29. ##游标为0表示没有key了
  30. if [ $cursor -eq 0 ];then
  31. signal=1
  32. fi
  33. ##循环数组
  34. for key in ${arr[@]}
  35. do
  36. echo $key
  37. if [ $key != $cursor ];then
  38. echo "key:"$key
  39. ##删除key
  40. redis-cli -h $host -p $port -c del $key >/dev/null 2>&1
  41. fi
  42. done
  43. done
  44. echo 'done'

使用方式:

./redis-del-keys.sh localhost 6379 user:*

表示删除本机6379端口的redis中user:开头的所以key。

8 总结

所以可以通过这个方法来查询库大的整个库的key,如果库小的话直接keys *搞定

可以使用下面的脚本来得到结果

单机版:

  1. #!/bin/bash
  2. redis-cli -h 127.0.0.1 -p 6379 scan 0 > keys.txt
  3. a=`head -1 keys.txt`
  4. while [ $a -ne 0 ]
  5. do
  6. redis-cli -h 127.0.0.1 -p 6379 scan $a > key.txt
  7. redis-cli -h 127.0.0.1 -p 6379 scan $a >> keys.txt
  8. a=`head -1 key.txt`
  9. sleep 1
  10. done

集群版:

  1. #!/bin/bash
  2. redis_comm=/home/zcy/redis/bin/redis-cli
  3. #search redis cluster master IP:PORT
  4. redis_list=$($redis_comm -p 9001 cluster nodes | grep master | awk '{print $2}' | awk -F['@'] '{print $1}')
  5. for info in ${redis_list[@]}
  6. do
  7. ip=`echo $info | cut -d : -f 1`
  8. port=`echo $info | cut -d : -f 2`
  9. echo "Begin search: $ip:$port"
  10. $redis_comm -h $ip -p $port scan 0 > key.txt
  11. cat key.txt | sed '/^[0-9]/d' >> keys.txt
  12. a=`head -1 key.txt`
  13. while [ $a -ne 0 ]
  14. do
  15. $redis_comm -h $ip -p $port scan $a > key.txt
  16. cat key.txt | sed '/^[0-9]/d' >> keys.txt
  17. a=`head -1 key.txt`
  18. sleep 0.2
  19. done
  20. done
  21. rm key.txt

其中,sed '/^[0-9]/d'是删除数字开头的行。

如果指定库的话改为 redis-cli -h 127.0.0.1 -p 6379 -n 1 scan 0 > keys.txt

最后总结:

  1. rehash从小到大时,scan系列命令不会重复也不会遗漏.

  2. 而rehash从大到小时,有可能会造成重复但不会遗漏.

  3. SCAN操作能够保证 一直没变动过的元素一定能够在扫描结束的之前返回给客户端,这一点在不同情况下都可以实现;

  4. 当发生字典大小缩小的时候,如果接受到一个scan cursor, 游标位于高位为1的部分,那么会被有效位掩码给注释最高位,从而从重新读取之前已经访问过的元素,这种情况下回发生数据重复,但应该有限;
     

整体来看redis的SCAN操作是很不错的,能够在hash的数据结构里面,不锁定库,稳定提供读写操作的前提下,提供比较稳定可靠的SCAN操作,不过大家再使用的时候最好知道他的优缺点,别太依靠scan做要求一致性太高的操作。

后端服务开发就是这样,可选的框架,开源实现很多,最好知其所以然并高效利用其优点,少被缺点坑了最关键。

 

参考:

http://chenzhenianqing.com/articles/1410.html

https://segmentfault.com/a/1190000018218584

https://www.jianshu.com/p/be15dc89a3e8

https://www.jianshu.com/p/a2d4d2eb4e14

https://www.jianshu.com/p/07c216db6e09

https://blog.51cto.com/chinahao/2056082

请我喝咖啡

如果觉得文章写得不错,能对你有帮助,可以扫描我的微信二维码请我喝咖啡哦~~哈哈~~

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

闽ICP备14008679号