Redis中的SCAN渐进式扫描底层原理

Scan渐进式扫描原理

概述

由于Redis是单线程再处理用户的命令,而Keys命令会一次性遍历所有key,于是在命令执行过程中,无法执行其他命令。这就导致如果Redis中的key比较多,那么Keys命令执行时间就会比较长,从而阻塞Redis,所以推荐使用Scan命令来代替Keys,因为Scan可以限制每次遍历的key数量。

Keys的缺点:

  • 1.没有limit,我们只能一次性获取所有符合条件的key,如果结果有上百万挑,那么等待的就是"无穷无尽"的字符串输出
  • 2.keys命令是遍历算法,时间复杂度是O(N)。这个命令非常容易导致Redis服务卡顿,要尽量避免在生产环境使用该命令。

Scan命令有两个比较明显的优势:

  • 1.Scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程
  • 2.Scan命令提供了count参数,可以控制每次遍历的集合数

可以理解为Scan是渐进式的keys.

大致用法

SCAN命令是基于游标的,每次调用后,都会返回一个游标,用于下一次迭代。当游标返回0时,表示迭代结束。第一次Scan时指定游标为0,表示开启新的一轮迭代,然后Scan命令返回一个新的游标,作为第二次Scan时的游标值继续迭代,一直到Scan返回游标为0,表示本轮迭代结束

通过这个就可以看出,Scan完成一次迭代,需要和Redis进行多次交互。

注意事项

  • 1.返回的结果可能会有重复,需要客户端去重复,这点非常重要
  • 2.遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的
  • 3.单词返回的结果是空的,并不意味着遍历结束,而要看返回的游标值是否为零

Scan使用案例

使用Scan命令,Count参数指定1000,Redis命中几百万Key.
这里会出现一个问题。Scan命令中的Count指定一次扫描多少key,这里指定为1000,
几百万key就需要几千次迭代,即和Redis交互几千次,再加上网络连接中的数据传输
开销和延迟,将会导致耗时比较长。这就需要将Count参数调大后,减少了交互次数。

Count参数越大,Redis阻塞时间也会越长,需要取舍。如果我们极端一点的话,
Count参数和总Key数一致时,Scan命令就和Keys效果一样了
在这里插入图片描述
Count大小和Scan总耗时的关系如图所示,
可以发现Count越大,总耗时就越短,不过后面提升就越不明显了
所以推荐的Count大小为1W左右.如果不考虑Redis的阻塞,其实Keys比Scan会快很多,毕竟是一次性处理,省去了多余的交互

Scan原理

Redis使用了Hash表作为底层实现,原因不外乎高校且实现简单。类似于HashMap那样数组+链表的结构.其中第一维的数组大小为2n(n>=0),每次扩容数组长度扩大一倍。Scan命令就是对这个一维数组进行遍历。
每次返回的游标值也都是这个数组的索引,Count参数表示遍历多少个数组的元素,将这些元素下挂接的符合条件的结果都返回。因为每隔元素下挂接的链表大小不同,所以每次返回的结果数量也就不同。

127.0.0.1:6379> keys *
1) "db_number"
2) "key1"
3) "myKey"
127.0.0.1:6379> scan 0 MATCH * COUNT 1
1) "2"
2) 1) "db_number"
127.0.0.1:6379> scan 2 MATCH * COUNT 1
1) "1"
2) 1) "myKey"
127.0.0.1:6379> scan 1 MATCH * COUNT 1
1) "3"
2) 1) "key1"
127.0.0.1:6379> scan 3 MATCH * COUNT 1
1) "0"
2) (empty list or set)

如代码所示,SCAN的命令额度遍历顺序是0->2->1->3
这个顺序看起来有些奇怪,把它转换成二进制:00->10->01->11,可以看到这个序列是最高位加1,
普通二进制的加法,是从右往左3相加、进位。而这个序列是从左往右相加、进位的
相关源码:

v = rev(v);
v++;
v = rev(v);

reverse binary iteration算法

Redis Scan命令最终使用的是reverse binnary iteration算法,大概可以翻译为逆二进制迭代。这个算法简单来说就是:
依次从高位(有效位)开始,不断尝试将当前高位设置为1,然后变动更高位为不同组合,依次来扫描整个字典数组
其最大的优势在于,从高位扫描的时候,如果槽位是2^N个,扫描的临近的2个元素都是与2 ^ (N-1)相关的就是说同模的,比如槽位8时,0%4 == 4 % 4, 1 % 4 == 5%4。因此想到其实hash的时候,跟模是很相关的。

比如当整个字典大小只有4的时候,一个元素计算出的整数为5,那么计算它的hash值需要模4,也就是hash(n) == 5 % 4 == 1,元素放在第一个槽位中。当字典进行扩容的时候,字典大小变为8,此时计算hash的时候为 5 % 8 == 5,该元素从1号slot迁移到了5号,1和5是对应的,我们称之为同模或者对应。同模的槽位的元素最容易出现合并或者拆分了。因此在迭代的时候只要及时地扫描这些相关地槽位,这样就不会造成大面积的重复扫描。

迭代时的三种情况

迭代哈希表时,有以下三种情况:

  • 1.从迭代开始到结束,哈希表不Rehash
  • 2.从迭代开始到结束,哈希表Rehash,但每次迭代,哈希要么不开始Rehash,要么已经结束Rehash
  • 3.从依次迭代开始到结束,哈希表在依次或多次迭代中Rehash,即再Rehash过程中,执行Scan命令,这时数据可能只迁移了一部分
第一种情况比较简单。

假设redis的hash表大小为4,第一个游标为0,读取第一个bucket
的数据,然后游标返回2,下次读取bucket 2,依次遍历

第二种情况更复杂。

假设redis的hash表为4,如果rehash后大小变成8.如果如上返回游标
(即返回2),则显示如图所示。
假设bucket 0读取后返回到cursor 2,当客户端再次Scan cursor 2时,hash表已经被rehash,大小翻倍到8,redis计算一个key bucket如下:

hash(key) & (size -1)

即如果大小为4,hash(key) & 11(3),如果大小为8,hash(key) & 111(7).所以当size从4扩大8时,2号bucket中的原始数据会被分散到2(010)和6(110)这两个bucket中。从二进制来看,size为4时,在hash(key)之后,取低两位,即hash(key) & 11,如果size为8,bucket位置为hash(key) & 111,即取低三位,所以不会出现漏掉数据的情况

第三种情况

如果返回游标2时正在进行rehash,则Hash表1的bucket2中的一些数据可能已经rehash到了Hash表2的bucket[2]或bucket[6],那么必须完全遍历哈希表2的bucket2和6,否则可能会丢失数据。Redis全局有两个Hash表,扩容时会渐进式地将表1地数据迁移到表2,查询时程序会先在ht[0]里面查找,如果没找到地话,就会继续到ht[1]里面进行查找

游标计算

Scan命中的游标,其实就是Redis内部地bucket

v |= ~m0 // 将游标v的unmarsked比特都置为1
v = rev(v); // 反转v
// 这个是关键,加1,对一个数加1,其实就是将这个数的低位的连续1变为0
// 然后将最低的一个0变为1,其实就是将最低的一个0变为1
v++; 
v= rev(v); // 再次反转,即得到下一个游标值

在这里插入图片描述

计算过程如图所示.
大小为4时,游标状态转换为0-2-13
当大小为8时,游标转台转换为0-4-2-6-1-5-3-7.
当size由小变大时,所有原来的游标都能在大HashTable中找到对应的位置,并且顺序一致,不会重复读取,也不会被遗漏。
总结:redis在rehash扩容的是时候,不会重复或者漏掉数据。但缩容,可能会造成重复,但不会漏掉数据

缩容处理

之所以会出现重复数据,其实就是为了保证缩容后数据不丢。
假设当前hash大小为8:

  • 1.第一次先遍历了bucket[0],返回游标为4
  • 2.准备遍历bucket[4],然后此时发生了缩容,bucket[4]的元素也进到了bucket[0]
  • 3.但是bucket[0]之前已经被遍历过了,此时会丢失数据吗?
    具体计算方法
v = (((v |m0) + 1) & (~m0) | (v & m0)

总结。

  • 1.Scan Count参数限制的是遍历的bucket数,而不是限制的返回的元素个数由于不同bucket中的元素个数不同,其中满足条件的个数也不同,所以每次Scan返回元素也不一定相同
  • 2.Count越大,Scan总耗时越短,但是单次耗时越大,即阻塞Redis时间变长
  • 2.1 推荐Count大小为1W左右
  • 2.2 当Count = Redis Key总数时,Scan和Keys效果一致
  • 3.Scan采用逆二进制发来计算游标,主要为了兼容Rehash的情况
  • 4.Scan为了兼容缩容后不漏掉数据,会出现重复遍历。需要客户端做去重处理

核心就是逆二进制迭代法,比较复杂,而且算法作者也没有具体证明,为什么这样就能实现,只是测试发现没有问题,各种情况都能兼容

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/730215.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

代码随想录三刷 day17 | 二叉树之110.平衡二叉树 257. 二叉树的所有路径 404.左叶子之和

三刷day17 110.平衡二叉树257. 二叉树的所有路径404.左叶子之和 110.平衡二叉树 题目链接 解题思路: 求高度用后续遍历,先得到左子树和右子树的情况,然后再进行判断(左右中)使用递归遍历 代码如下: class…

即插即用篇 | YOLOv8 引入 ParNetAttention 注意力机制 | 《NON-DEEP NETWORKS》

论文名称:《NON-DEEP NETWORKS》 论文地址:https://arxiv.org/pdf/2110.07641.pdf 代码地址:https://github.com/imankgoyal/NonDeepNetworks 文章目录 1 原理2 源代码3 添加方式4 模型 yaml 文件template-backbone.yamltemplate-small.yamltemplate-large.yaml

程序员常用的几种算法

程序员常用的几种算法 一、程序员算法汇总二、程序员常用的几种算法1.选择排序算法1.1 选择排序算法解析:1.2 示例代码: 2.插入排序算法2.1 插入排序算法解析:2.2 示例代码: 3.冒泡排序算法3.1 冒泡排序算法解析:3.2 示…

LeetCode买卖股票的最佳时机

文章目录 LeetCode买卖股票的最佳时机121 买卖股票的最佳时机Ⅰ题目描述代码 122 买卖股票的最佳时间Ⅱ题目描述代码 123 买卖股票的最佳时机Ⅲ题目描述代码一(会超时)代码二(dp) 188 买卖股票的最佳时机Ⅳ题目描述代码 LeetCode买…

题记(51)--L1-023 输出GPLT

目录 一、题目内容 二、输入描述 三、输出描述 四、输入输出示例 五、完整C语言代码 一、题目内容 给定一个长度不超过10000的、仅由英文字母构成的字符串。请将字符重新调整顺序,按GPLTGPLT....这样的顺序输出,并忽略其它字符。当然,四…

【PyTorch】进阶学习:探索BCEWithLogitsLoss的正确使用---二元分类问题中的logits与标签形状问题

【PyTorch】进阶学习:探索BCEWithLogitsLoss的正确使用—二元分类问题中的logits与标签形状问题 🌈 个人主页:高斯小哥 🔥 高质量专栏:Matplotlib之旅:零基础精通数据可视化、Python基础【高质量合集】、Py…

微服务架构 | 多级缓存

INDEX 通用设计概述2 优势3 最佳实践 通用设计概述 通用设计思路如下图 内容分发网络(CDN) 可以理解为一些服务器的副本,这些副本服务器可以广泛的部署在服务器提供服务的区域内,并存有服务器中的一些数据。 用户访问原始服务器…

(未解决)macOS matplotlib 中文是方框

reference: Mac OS系统下实现python matplotlib包绘图显示中文(亲测有效)_mac plt 中文值-CSDN博客 module ‘matplotlib.font_manager‘ has no attribute ‘_rebuild‘解决方法_font_manager未解析-CSDN博客 # 问题描述(笑死 显而易见 # solve 找到…

C++从零开始的打怪升级之路(day46)

这是关于一个普通双非本科大一学生的C的学习记录贴 在此前,我学了一点点C语言还有简单的数据结构,如果有小伙伴想和我一起学习的,可以私信我交流分享学习资料 那么开启正题 今天分享的是关于二叉树的题目 1.从前序与中序遍历序列构造二叉…

自编码器(Autoencoder, AE)

自编码器(Autoencoder, AE)是一种无监督学习算法,它利用神经网络来学习数据的高效表示(即编码)。自编码器的目标是能够通过输入数据学习到一个压缩的、分布式的表示,然后通过这种表示重构出原始数据。自编码器主要由两部分组成:编码器(Encoder)和解码器(Decoder)。 …

测试岗最好用的——十大软件测试工具

前言 目前由于软件测试工作在软件的生产过程中越来越重要,很多软件测试工具应运而生,这里介绍一下目前最流行的一些软件测试工具,一个十个,介绍如下:一、企业级自动化测试工具WinRunner 这款软件是Mercury Interacti…

【Linux】 yum —— Linux 的软件包管理器

Linux 的软件包管理器 yum yum 是什么什么是软件包查看软件包 yum 命令行工具yum 配置文件yum 凭什么可以支持下载呢?yum 生态yum 社区yum 的故障排除和资源支持yum 的持续集成和持续交付 yum 是什么 Yum(Yellowdog Updater Modified)是一个…

【PCIe】TLP结构与配置空间

🔥博客主页:PannLZ 文章目录 PCIe TLP结构PCIe配置空间和地址空间 PCIe TLP结构 TLP 主要由3个部分组成: Header 、 数据(可选,取决于具体的TLP 类 型 ) 和 ECRC (End to End CRC, 可选)。TLP 都始于发送端的事务层,终…

html 如何引入 百度地图

要在网页中创建和初始化一个地图&#xff0c;通常需要经过以下几个步骤&#xff1a;获取API密钥&#xff08;Access key 百度地图开放平台自行注册获取&#xff09;、加载API脚本、编写HTML、编写JS代码。 下面代码自行测试 html <!DOCTYPE html> <html> <he…

物联网,智慧城市的数字化转型引擎

随着科技的飞速发展&#xff0c;物联网&#xff08;IoT&#xff09;已成为推动智慧城市建设的关键力量。物联网技术通过连接各种设备和系统&#xff0c;实现数据的实时采集、传输和处理&#xff0c;为城市的智能化管理提供了强大的支持。在数字化转型的浪潮中&#xff0c;物联网…

【操作系统概念】 第8章:内存管理

文章目录 0.前言8.1 背景8.1.1 基本硬件8.1.2 地址绑定8.1.3 逻辑地址空间和物理地址空间8.1.4 动态加载&#xff08;dynamic loading&#xff09;8.1.5 动态链接&#xff08;dynamically linking&#xff09;与共享库 8.3 连续内存分配&#xff08;contiguous memory allocati…

【linuxC语言】dup、dup2函数

文章目录 前言一、dup函数二、dup2函数三、将标准输出重定向到文件总结 前言 在Linux环境下&#xff0c;dup、dup2以及原子操作都是用于文件描述符管理和处理的重要工具。这些功能提供了对文件描述符进行复制和原子操作的能力&#xff0c;使得在多线程或多进程环境中更加安全和…

10大主流压力/负载/性能测试工具推荐

在移动应用和Web服务正式发布之前&#xff0c;除了进行必要的功能测试和安全测试&#xff0c;为了保证互联网产品的服务交付质量&#xff0c;往往还需要做压力/负载/性能测试。然而很多传统企业在试水互联网的过程中&#xff0c;往往由于资源或产品迭代速度等原因忽视了这一块工…

整屋案例丨福州府108m²3室2厅2卫轻奢有度,高级耐看。福州中宅装饰,福州装修

空间之间的空间 比空间本身更具有意味&#xff0c; 但也容易被忽略&#xff0c; 正是由于“之间”的多元性和复杂性 以及它的不确定性&#xff0c; 空间之间变得无限可能。 平面设计图 项目信息 项目名称 | 福州府 设计地址 | 福建福州 项目面积 | 108㎡ 项目户型 | …

【JavaEE初阶】 JVM类加载简介

文章目录 &#x1f343;前言&#x1f332;类加载过程&#x1f6a9;加载&#x1f6a9;验证&#x1f6a9;准备&#x1f6a9;解析&#x1f6a9;初始化 &#x1f384;双亲委派模型&#x1f6a9;什么是双亲委派模型&#xff1f;&#x1f6a9;双亲委派模型的优点 ⭕总结 &#x1f343…