学习目标:

1、了解Scan的使用

2、掌握Scan和JRedis的Scan的使用

学习过程:

     在生产环境中经常需要查看某些key的值,已获取其对应的value,也经常需要查看又那些大key,影响了性能,在前面我们学习了keys 命令,可以通过模糊查询获得对应对应的所有的key,如果生产环境的key很多,那么keys * 会很慢,因为redis是单线程的,这个keys 命令会导致redis卡顿,所以一般来说keys 命令都是禁用的,从redis 2.8后,推荐使用scan命令。scan命令是增量的循环,类似mysql的分页查询,每次调用只会返回一小部分的元素。所以不会有keys命令的问题。scan命令返回的是一个游标,从0开始遍历,到0结束遍历。

命令格式:

SCAN cursor [MATCH pattern] [COUNT count]
命令解释:scan 游标 MATCH <返回和给定模式相匹配的元素> count 每次迭代所返回的元素数量
通过scan中的MATCH <pattern> 参数,可以让命令只返回和给定模式相匹配的元素,实现模糊查询的效果

一、redis的命令行界面

先添加100个

@Test  
public void testSpringRedis2() {  
	for(int i=0;i<100;i++) {
		template.opsForValue().set("name"+i, "liubao"+1);
	}
}

redis测试,下面的测试比较长,可以直接看到最后一个了。坐标返回为0就标识全部编译结束了

127.0.0.1:6379> scan 0 match name* count 10
 1) "132"
 2)  1) "name23"
     2) "name71"
     3) "name60"
     4) "name50"
     5) "name77"
     6) "name56"
     7) "name53"
     8) "name31"
     9) "name22"
    10) "name18"
 127.0.0.1:6379> scan 132 match name* count 10
 1) "66"
 2)  1) "name64"
     2) "name67"
     3) "name0"
     4) "name59"
     5) "name10"
     6) "name6"
     7) "name29"
     8) "name80"
     9) "name91"
    10) "name40"
 127.0.0.1:6379> scan 66 match name* count 10
 1) "234"
 2)  1) "name34"
     2) "name36"
     3) "name49"
     4) "name51"
     5) "name63"
     6) "name30"
     7) "name76"
     8) "name9"
     9) "name44"
    10) "name85"
    11) "name89"
 127.0.0.1:6379> scan 234 match name* count 10
 1) "150"
 2)  1) "name13"
     2) "name38"
     3) "name11"
     4) "name74"
     5) "name55"
     6) "name81"
     7) "name37"
     8) "name86"
     9) "name48"
    10) "name41"
 127.0.0.1:6379> scan 150 match name* count 10
 1) "254"
 2)  1) "name45"
     2) "name96"
     3) "name35"
     4) "name58"
     5) "name93"
     6) "name79"
     7) "name84"
     8) "name62"
     9) "name4"
    10) "name1"
 127.0.0.1:6379> scan 254 match name* count 10
 1) "77"
 2) 1) "name32"
    2) "name54"
    3) "name75"
    4) "name27"
    5) "name24"
    6) "name39"
    7) "name26"
    8) "name72"
    9) "name3"
 127.0.0.1:6379> scan 77 match name* count 10
 1) "131"
 2)  1) "name98"
     2) "name69"
     3) "name2"
     4) "name70"
     5) "name25"
     6) "name78"
     7) "name88"
     8) "name7"
     9) "name15"
    10) "name66"
 127.0.0.1:6379> scan 131 match name* count 10
 1) "155"
 2)  1) "name92"
     2) "name47"
     3) "name83"
     4) "name5"
     5) "name33"
     6) "name8"
     7) "name87"
     8) "name20"
     9) "name43"
    10) "name65"
 127.0.0.1:6379> scan 155 match name* count 10
 1) "23"
 2)  1) "name97"
     2) "name61"
     3) "name16"
     4) "name94"
     5) "name19"
     6) "name"
     7) "name52"
     8) "name90"
     9) "name99"
    10) "name73"
    11) "name82"
    12) "name95"
 127.0.0.1:6379> scan 23 match name* count 10
 1) "95"
 2) 1) "name14"
    2) "name57"
    3) "name21"
    4) "name12"
    5) "name46"
    6) "name68"
    7) "name17"
    8) "name28"
 127.0.0.1:6379> scan 95 match name* count 10
 1) "0"   #返回0就是遍历结束了。
 2) 1) "name42"你会发现每一次返回的并不都是10条数据,所以和mysql的分页查询还是又区别的。一直遍历到返回的 cursor 值为 0 时结束。
hash也同样支持scan,命令也类似hasc,示例如下:
127.0.0.1:6379> hset students name1 liu1 name2 liu2 name3 liu3
 (integer) 3
 127.0.0.1:6379> hscan students 0 match name* count 10
 1) "0"
 2) 1) "name1"
    2) "liu1"
    3) "name2"
    4) "liu2"
    5) "name3"
    6) "liu3"

二、遍历顺序

大家可以看到scan的遍历并不是一次性取得所有的数据,那么如果在遍历的过程中,发生了扩容,如果保证遍历的数据没有重复和遗漏呢。scan不是从第一维数组的第 0 位一直遍历到末尾,而是采用了高位进位加法来遍历。这样就可以解决到字典的扩容和缩容时避免槽位的遍历重复和遗漏。高位进位法从左边加,进位往右边移动,高位进位法可以使得扩容后并没有改变原来的排序,同普通加法正好相反,所以最终它们都会遍历所有的槽位并且没有重复,有兴趣的同学可以自己去了解一下,

三、定位大 key --bigkeys

前面说了为了避免对生产环境的 Redis服务 造成卡顿,我们经常需要查找那些大的key,不过 Redis 官方已经在 redis-cli 指令中提供了这样的扫描功能,我们可以直接拿来即用。

redis-cli -a 123456 –-bigkeys

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

redis-cli  –-bigkeys -i 0.1

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

四、java封装的scan

示例代码如下:

@Test  
public void  testScan() {  
	
	final String query="name*";

	template.execute(new RedisCallback<Object>() {

		@Override
		public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
			ScanOptions options = ScanOptions.scanOptions().match(query).count(Integer.MAX_VALUE).build();
	        Cursor<byte[]> c = redisConnection.scan(options);
	        while (c.hasNext()) {
	            System.out.println(new String((byte[]) c.next()));
	        }
			return null;
		}
	});
}

ursor一定不能关闭,在之前的版本中,这里Cursor需要手动关闭,但是从1.8.0开始,不能手动关闭!否则会报异常。