文章目录

  • 需求
  • scan
  • scan基本使用
    • 批量写入一批模拟数据
  • 字典的结构
  • scan 遍历顺序 (高位进位法)
  • 渐进式 rehash
  • 更多的 scan 指令
  • 大 key 扫描 --bigkeys
  • 使用scan的注意事项 20201101更新


需求

假设你需要从 Redis 实例成千上万的 key 中找出特定前缀的 key 列表来手动处理数据,可能是修改它的值,也可能是删除 key。那该如何从海量的 key 中找出满足特定前缀的 key 列表来?

我们可以用 keys 来列出所有满足特定正则字符串规则的 key .

192.168.18.131:8001> set artisan 1
OK
192.168.18.131:8001> set artisan2 2
-> Redirected to slot [6066] located at 192.168.18.132:8002
OK
192.168.18.132:8002> set artisan3 3
-> Redirected to slot [1939] located at 192.168.18.131:8001
OK
192.168.18.131:8001> set artisan4 4
-> Redirected to slot [14196] located at 192.168.18.132:8005
OK
192.168.18.132:8005> set artisan5 5
-> Redirected to slot [10069] located at 192.168.18.132:8002
OK
192.168.18.132:8002> 
192.168.18.132:8002> keys *
1) "artisanKey"
2) "clusterArtisan"
3) "artisan2"
4) "ar"
5) "test"
6) "artisan5"
192.168.18.132:8002> keys artisan*
1) "artisanKey"
2) "artisan2"
3) "artisan5"
192.168.18.132:8002> 

我这个是集群环境,有几个key被分到了其他的slot上去了,所以看到的数据仅仅是当前slot的数据。

keys 优点呢 ,使用简单

当然了,也有缺点

  • 一次性列出所有满足条件的 key. keys 算法是遍历算法,复杂度是 O(n) ,如果数据量很大,会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,因为Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续。

咋办呢? ---------------> Redis在 2.8 版本中加入了scan指令.


scan

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

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

scan基本使用

批量写入一批模拟数据


import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;import java.io.IOException;
import java.util.HashSet;
import java.util.Set;public class JedisClusterDemo {public static void main(String[] args) throws IOException {JedisPoolConfig config = new JedisPoolConfig();config.setMaxTotal(20);config.setMaxIdle(10);config.setMinIdle(5);Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();jedisClusterNode.add(new HostAndPort("192.168.18.131", 8001));jedisClusterNode.add(new HostAndPort("192.168.18.131", 8004));jedisClusterNode.add(new HostAndPort("192.168.18.132", 8002));jedisClusterNode.add(new HostAndPort("192.168.18.132", 8005));jedisClusterNode.add(new HostAndPort("192.168.18.133", 8003));jedisClusterNode.add(new HostAndPort("192.168.18.133", 8006));JedisCluster jedisCluster = null;try {//connectionTimeout:指的是连接一个url的连接等待时间//soTimeout:指的是连接上一个url,获取response的返回等待时间jedisCluster = new JedisCluster(jedisClusterNode, 6000, 5000, 10, "artisan", config);for (int i = 0; i < 10000; i++) {jedisCluster.set("{art}:clusterArtisan:" + i, "artisanValue:" + i);}System.out.println("DONE");} catch (Exception e) {e.printStackTrace();} finally {if (jedisCluster != null)jedisCluster.close();}}
}


scan 参数提供了三个参数:

  • 第一个是 cursor 整数值

  • 第二个是 key 的正则模式

  • 第三个是遍历的 limit hint。

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

192.168.18.132:8005> scan 0 match {art}:clusterArtisan:9* count 1000
1) "13848"
2)   1) "{art}:clusterArtisan:9927"2) "{art}:clusterArtisan:9660"3) "{art}:clusterArtisan:994"4) "{art}:clusterArtisan:934".................129) "{art}:clusterArtisan:9830"130) "{art}:clusterArtisan:9470"
192.168.18.132:8005> 

将第一次的返回结果 13848 ,作为第二次的入参

192.168.18.132:8005> scan  13848  match {art}:clusterArtisan:9* count 1000
1) "11596"
2)   1) "{art}:clusterArtisan:9347"2) "{art}:clusterArtisan:9737"3) "{art}:clusterArtisan:9943"4) "{art}:clusterArtisan:9944".................122) "{art}:clusterArtisan:9744"123) "{art}:clusterArtisan:9256"
192.168.18.132:8005> 

省略过程 …

依次,获取 ,直到cursor为0 ,遍历结束

192.168.18.132:8005> scan  13209  match {art}:clusterArtisan:9* count 10000
1) "0"
2)   1) "{art}:clusterArtisan:927"2) "{art}:clusterArtisan:9474"3) "{art}:clusterArtisan:9685"........................424) "{art}:clusterArtisan:9424"
192.168.18.132:8005> 

我们发现limit 是 1000,但是返回的结果并不是返回1000个。因为这个 limit 不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量(约等于)。

如果将 limit 设置为 10,你会发现返回结果是空的,但是游标值不为零,意味着遍历还没结束。

比如

192.168.18.132:8005> scan  13822  match {art}:clusterArtisan:999* count 1000
1) "13209"
2) (empty list or set)
192.168.18.132:8005> 

字典的结构

在 Redis 中所有的 key 都存储在一个很大的字典中.

这个字典的结构和 Java 中的HashMap 一样,是一维数组 + 二维链表结构.

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

scan 指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽 (slot)。

如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。

limit 参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。

每一次遍历都会将 limit数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端。


scan 遍历顺序 (高位进位法)

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

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


渐进式 rehash

Java 的 HashMap 在扩容时会一次性将旧数组下挂接的元素全部转移到新数组下面。

如果 HashMap 中元素特别多,线程就会出现卡顿现象。Redis 为了解决这个问题,它采用渐进式 rehash。

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

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


更多的 scan 指令

scan 指令是一系列指令,除了可以遍历所有的 key 之外,还可以对指定的容器集合进行遍历。

比如 zscan 遍历 zset 集合元素,hscan 遍历 hash 字典的元素、sscan 遍历 set 集
合的元素。

它们的原理同 scan 都会类似的,因为 hash 底层就是字典,set 也是一个特殊的
hash(所有的 value 指向同一个元素),zset 内部也使用了字典来存储所有的元素内容.


大 key 扫描 --bigkeys

[redis@artisan redis-5.0.3]$ ./bin/redis-cli -c -h 192.168.18.131 -p 8001 -a artisan --bigkeys
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).[00.00%] Biggest string found so far 'artisan3' with 1 bytes-------- summary -------Sampled 2 keys in the keyspace!
Total key length in bytes is 15 (avg len 7.50)Biggest string found 'artisan3' has 1 bytes2 strings with 2 bytes (100.00% of keys, avg size 1.00)
0 lists with 0 items (00.00% of keys, avg size 0.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
[redis@artisan redis-5.0.3]$ 

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

--bigkeys -i 0.1

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

[redis@artisan redis-5.0.3]$ ./bin/redis-cli -c -h 192.168.18.131 -p 8001 -a artisan --bigkeys  -i 0.1

使用scan的注意事项 20201101更新

如果在scan的过程中如果有键的变化(增加、 删除、 修改) ,遍历效果可能会碰到如下问题: 新增的键可能没有遍历到, 遍历出了重复的键等情况, 也就是说scan并不能保证完整的遍历出来所有的键, 我们在使用的过程中需要考虑到这一点。

Redis进阶-如何从海量的 key 中找出特定的key列表 Scan详解相关推荐

  1. 海量数据处理 - 10亿个数中找出最大的10000个数(top K)

    海量数据处理 - 10亿个数中找出最大的10000个数(top K问题) 版权声明:本文为博主原创文章,未经博主允许不得转载 前两天面试3面学长问我的这个问题(想说TEG的3个面试学长都是好和蔼,希望 ...

  2. 海量数据处理 - 10亿个数中找出最大的10000个数(top K问题)

    top K问题 在大规模数据处理中,经常会遇到的一类问题:在海量数据中找出出现频率最好的前k个数,或者从海量数据中找出最大的前k个数,这类问题通常被称为top K问题.例如,在搜索引擎中,统计搜索最热 ...

  3. 海量数据处理:在100亿个数中找出top 10000

    经典的TOP K问题,借助堆排序进行 前两天面试3面学长问我的这个问题(想说TEG的3个面试学长都是好和蔼,希望能完成最后一面,各方面原因造成我无比想去鹅场的心已经按捺不住了),这个问题还是建立最小堆 ...

  4. java中属性文件读取案例_java相关:Spring中属性文件properties的读取与使用详解

    java相关:Spring中属性文件properties的读取与使用详解 发布于 2020-6-3| 复制链接 摘记: Spring中属性文件properties的读取与使用详解实际项目中,通常将一些 ...

  5. 二叉树路径和最大python_python3实现在二叉树中找出和为某一值的所有路径(推荐)...

    请写一个程序创建一棵二叉树,并按照一定规则,输出二叉树根节点到叶子节点的路径. 规则如下: 1.从最顶端的根结点,到最下面的叶子节点,计算路径通过的所有节点的和,如果与设置的某一值的相同,那么输出这条 ...

  6. python中的列表分片详解_python列表与元组详解实例

    在这章中引入了数据结构的概念.数据结构是通过某种方式组织在一起的数据元素的集合.在python中,最基本的数据结构就是序列.序列中的每个元素被分配一个序号,即元素的位置,也被称为索引.注意:第一个索引 ...

  7. 数据结构与算法--有序数组中找出和为s的两个数字

    有序数组中找和为s的两个数字 题目:输入一个递增排序的数组array, 和一个数字s, 在数组中找出两个数,使得这两个数的和是s,如果有多对,输出一对即可. 最简单方案 双循环,每次获取一个数据,和数 ...

  8. 10亿个数中找出最大的10000个数

    转载自  海量数据处理 - 10亿个数中找出最大的10000个数(top K问题) 前两天面试3面学长问我的这个问题(想说TEG的3个面试学长都是好和蔼,希望能完成最后一面,各方面原因造成我无比想去鹅 ...

  9. 找出一个字符串中出现次数最多的字_海量数据中找出前k大数(topk问题)

    在海量数据中找出出现频率最好的前k个数,或者从海量数据中找出最大的前k个数,这类问题通常被称为top K问题. 针对top K类问题,通常比较好的方案是分治+Trie树/hash+小顶堆(就是上面提到 ...

最新文章

  1. 10个例子带你了解机器学习中的线性代数
  2. python 元组使用_Python3
  3. python模拟手写_python-自己手写的贴吧爬虫
  4. 安装phproject之一
  5. 爆牙齿的世界杯日记(小组首轮)
  6. angular4 表单
  7. 设置TextView文字
  8. “约见”面试官系列之常见面试题之第一百零八篇之如何获取dom(建议收藏)
  9. Delphi-IOCP学习笔记九======性能测试和IOCP源码下载5000个连接数
  10. 计算机在医疗领域的发展未来,5G医疗在人工智能领域未来的发展
  11. Sipp命令行参数说明
  12. Hikari 数据库连接池配置详解
  13. 用texstudio写伪代码
  14. 一万八的M1 iPad Pro ,怎么就成了“期货”
  15. 可重入函数与线程安全的区别与联系
  16. 达芬奇密码 第七十一章
  17. 被final修饰的变量到底能不能被修改
  18. 双11电商技术:不再谷满谷,坑满坑,看苏宁库存架构转变
  19. python wpf_为什么很少看到使用WPF开发的流行的应用程序呢?
  20. windows安全警报怎么关闭_鄂州人注意!本周五上午,全市将进行防空警报鸣放...

热门文章

  1. bash 判断 os 版本_鸿蒙OS手机将至,华为手机用户提问,老款机器能升级吗?
  2. vins中imu融合_双目版 VINS 项目发布,小觅双目摄像头作为双目惯导相机被推荐...
  3. zip()和zip(*)的区别与使用
  4. Spark DataFrameDataSet
  5. F5 BIG-IP 远程代码执行漏洞 CVE-2021-22986 虚拟机上复现
  6. NTU 课程辅助笔记: NFA到DFA的转化
  7. 模型独立学习:多任务学习与迁移学习
  8. Flink从入门到精通100篇(二)-在Linux中完整安装flink并做Flink文件的配置
  9. 机器学习第5天:逻辑回归
  10. 人工智能的发展趋势和行业岗位