

假设你需要从 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 优点呢 ,使用简单
当然了,也有缺点
咋办呢? ---------------> Redis在 2.8 版本中加入了scan指令.
scan 相比keys 具备有以下特点:
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 值为 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 的遍历顺序非常特别。它不是从第一维数组的第 0 位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏.
高位进位法从左边加,进位往右边移动,同普通加法正好相反。但是最终它们都会遍历所有的槽位并且没有重复。
Java 的 HashMap 在扩容时会一次性将旧数组下挂接的元素全部转移到新数组下面。
如果 HashMap 中元素特别多,线程就会出现卡顿现象。Redis 为了解决这个问题,它采用渐进式 rehash。
它会同时保留旧数组和新数组,然后在定时任务中以及后续对 hash 的指令操作中渐渐地将旧数组中挂接的元素迁移到新数组上。这意味着要操作处于 rehash 中的字典,需要同时访问新旧两个数组结构。如果在旧数组下面找不到元素,还需要去新数组下面去寻找。
scan 也需要考虑这个问题,对与 rehash 中的字典,它需要同时扫描新旧槽位,然后将结果融合后返回给客户端。
scan 指令是一系列指令,除了可以遍历所有的 key 之外,还可以对指定的容器集合进行遍历。
比如 zscan 遍历 zset 集合元素,hscan 遍历 hash 字典的元素、sscan 遍历 set 集 合的元素。
它们的原理同 scan 都会类似的,因为 hash 底层就是字典,set 也是一个特殊的 hash(所有的 value 指向同一个元素),zset 内部也使用了字典来存储所有的元素内容.
[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 bytes
2 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的过程中如果有键的变化(增加、 删除、 修改) ,遍历效果可能会碰到如下问题: 新增的键可能没有遍历到, 遍历出了重复的键等情况, 也就是说scan并不能保证完整的遍历出来所有的键, 我们在使用的过程中需要考虑到这一点。