Redis深度历险-Redis Scan

本文深入解析Redis中的SCAN命令,探讨其如何避免了KEYS命令的性能瓶颈,通过游标分步遍历,解决了大规模数据扫描时的阻塞问题。文章还介绍了SCAN命令的遍历顺序、渐进式rehash机制及如何处理大key扫描,为Redis优化提供了实用指导。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文大部分内容引自《Redis深度历险:核心原理和应用实践》,感谢作者!!!

当要找出所有满足特定正则字符串规则的key时应该用什么命令?

keys * #找出满足特定正则字符串规则的key

keys命令的缺点

1、没有偏移量offset、限定数量limit,一次性找出所有满足条件的key,如果实例中有百万个满足条件的key则会影响性能

2、keys命令的算法是遍历算法,复杂度是O(n),如果实例中有千万级别以上的key,则会导致Redis服务卡顿,所有Redis读写的指令都会被延后甚至超时报错,因为Redis是单线程程序,顺序执行所有指令,其他指令必须等到当前的keys执行完毕后才可以继续

Scan命令的特点

1、复杂度是O(n),通过游标分步进行不会阻塞线程

2、提供limit参数,可以控制每回返回结果的最大条数,limit只是一个hint(复数)

3、可以使用正则表达式筛选结果

4、服务器不需要为游标保存状态,游标的唯一状态就是scan返回给客户端的游标整数

5、返回的结果可能会有重复,需要客户端去重

6、遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的

7、单次返回为空的结果并不意味着遍历结束,而是要看返回的游标值是否为零

Scan的使用

Scan的使用讲解:https://www.jianshu.com/p/be15dc89a3e8

scan [cursor] match [pattern] limit [count] #从cursor(包含)开始遍历数量为count哈希槽中的所有的key,找出符合pattern的结果

 

 

limit的数量是1000但是返回的结果集只有10左右的原因

1、Redis中所有的key都是以HashMap形式的字典存在的,数据结构是一维数组 + 二维链表,第一维数组的总大小是2^n,扩容一次数组大小空间加倍

 

2、scan指令返回的游标是一维数组的位置索引,称这个索引为slot(槽),limit表示要变里的槽位数

Scan遍历顺序

scan遍历顺序是采用高位进位加法来遍历的(00 -> 10 -> 01 -> 11),原因是考虑到了字典的扩容与缩容是避免槽位的遍历重复和遗漏

字典扩容

1、Java中的HashMap有扩容的概念,当loadFactor达到阈值时,需要重新分配一个新的2倍大小的数组,然后将所有的元素全部rehash挂到新的数组下面。rehash就是将元素的hash值对数组长度进行取模运算,因为长度变了,所以每个元素挂接的槽位可能也发生了变化。又因为数组的长度是 2^n 次方,所以取模运算等价于位与操作

2、扩容翻倍,相当于原有值多除以了一个2,即二进制位数向右移了一位(高位补零,最高位清零),原有数字的二进制和2^n-1二进制做与运算

a mod 8 = a & (8-1) = a & 7 
a mod 16 = a & (16-1) = a & 15 
a mod 32 = a & (32-1) = a & 31 #7, 15, 31 称之为字典的 mask 值,mask 的作用就是保留 hash 值的低位,高位都被设置为 0

1、当前的字典的数组长度由8位扩容到16位,那么3号槽位011将会被rehash到3号槽位和11号槽位,也就是说该槽位链表中大约有一半的元素还是3号槽位,其它的元素会放到11号槽位,11这个数字的二进制是1011,就是对3 的二进制011增加了一个高位1

 

2、假设开始槽位的二进制数是xxx,那么该槽位中的元素将被rehash到0xxx和1xxx(xxx+8) 中。如果字典长度由16位扩容到32位,那么对于二进制槽位xxxx中的元素将被rehash到0xxxx和1xxxx(xxxx+16)中

对比扩容缩容前后的遍历顺序

 

1、采用高位进位加法的遍历顺序,rehash后的槽位在遍历顺序上是相邻的

2、假设当前要即将遍历 110 这个位置 (橙色),那么扩容后,当前槽位上所有的元素对应的新槽位是0110和1110(深绿色),也就是在槽位的二进制数增加一个高位0或1。这时我们可以直接从0110这个槽位开始往后继续遍历,0110槽位之前的所有槽位都是已经遍历过的,这样就可以避免扩容后对已经遍历过的槽位进行重复遍历

3、再考虑缩容,假设当前即将遍历110这个位置(橙色),那么缩容后,当前槽位所有的元素对应的新槽位是10(深绿色),也就是去掉槽位二进制最高位。这时我们可以直接从10这个槽位继续往后遍历,10槽位之前的所有槽位都是已经遍历过的,这样就可以避免缩容的重复遍历。不过缩容还是不太一样,它会对图中010这个槽位上的元素进行重复遍历,因为缩融后10槽位的元素是010和110上挂接的元素的融合

渐进式rehash

在redis中,扩展或收缩哈希表需要将ht[0]里面的所有键值对rehash到ht[1]里面,但是这个rehash动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1] ,而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]

以下是哈希表渐进式 rehash 的详细步骤:

1、为ht[1] 分配空间,让字典同时持有ht[0]和ht[1]两个哈希表

2、在字典中维持一个索引计数器变量rehashidx ,并将它的值设置为0 ,表示rehash工作正式开始

3、在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1] ,当rehash工作完成之后,程序将rehashidx 属性的值增一

4、随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1 , 表示rehash操作已完成

渐进式 rehash 执行期间的哈希表操作

1、渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量

2、在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行:比如说,要在字典里面查找一个键的话,程序会先在ht[0]里面进行查找,如果没找到的话,就会继续到ht[1]里面进行查找,诸如此类

3、另外,在渐进式rehash执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作:这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表

渐进式rehash带来的问题

渐进式rehash避免了redis阻塞,可以说非常完美,但是由于在rehash时需要分配一个新的hash表,在rehash期间同时有两个hash表在使用,会使得redis内存使用量瞬间突增,在Redis满容状态下由于rehash会导致大量key驱逐

scan指令扩展以及大key扫描、定位

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

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

3、如果观察到Redis的内存大起大落,这极有可能是因为大key导致的

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

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值