本文大部分内容引自《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