在生产环境中使用了keys指令之后容易导致出现短时间内的请求堵塞,这种情况在高并发环境中是比较致命的存在,因此需要尽可能地避免这种情况发生。
常用的查询某些key的指令:
scan
jedis使用方式:
public List<String> scanAll(String cursor, String pattern, Integer limit) { try (Jedis jedis = iRedisFactory.getConnection()) { List<String> scanList = new LinkedList<>(); ScanParams scanParams = new ScanParams(); scanParams.match(pattern); scanParams.count(limit); while (true) { long begin = System.currentTimeMillis(); ScanResult<String> scanResult = jedis.scan(cursor, scanParams); if (ScanParams.SCAN_POINTER_START.equals(scanResult.getCursor())) { break; } System.out.println("耗时:" + (System.currentTimeMillis() - begin) + "ms,查询数目:" + scanResult.getResult().size()); scanList.addAll(scanResult.getResult()); cursor = scanResult.getCursor(); } return scanList; } catch (Exception e) { log.error("jedis scanAll has error, error is ", e); } return null; } 复制代码
虽然在使用scan指令的时候复杂度也是o(n)会有部分堵塞,但是由于是多次请求,相当于将之前的keys指令分成了多次小范围的搜索,减少堵塞的时长。
\
sscan
jedis使用方式
public List<String> sScanAll(String key,String cursor, String pattern, Integer limit) { try(Jedis jedis = iRedisFactory.getConnection()) { List<String> resultList = new LinkedList<>(); while (true) { ScanResult<String> scanResult = jedis.sscan(key,cursor); cursor = scanResult.getCursor(); resultList.addAll(scanResult.getResult()); if (ScanParams.SCAN_POINTER_START.equals(cursor)){ break; } } return resultList; }catch (Exception e){ log.error("jedis sscanAll has error, error is ", e); } return null; } 复制代码
其实sscan指令结合名字就可以猜到,这是一个用于set集合使用的遍历指令,比较推荐在对set集合中进行遍历使用,例如当我们的set集合元素过多的时候,直接使用smembers指令容易造成堵塞,此时使用sscan指令可以减少堵塞的情况发生。
\
hscan
jedis使用方式
public Map<String,String> hScanAll(String key, String cursor, String pattern, Integer limit) { try(Jedis jedis = iRedisFactory.getConnection()) { ScanParams scanParams = new ScanParams(); scanParams.match(pattern); scanParams.count(limit); Map<String,String> resultList = new HashMap<>(); while (true) { ScanResult scanResult = jedis.hscan(key,cursor,scanParams); cursor = scanResult.getCursor(); if (ScanParams.SCAN_POINTER_START.equals(cursor)){ break; } List<Map.Entry<String,String>> result = scanResult.getResult(); for (Map.Entry<String, String> entry : result) { resultList.put(entry.getKey(),entry.getValue()); } } return resultList; }catch (Exception e){ log.error("jedis sscanAll has error, error is ", e); } return null; } 复制代码
这是一个用于hashmap集合使用的遍历指令
\
zscan
jedis使用方式
public List<String> zScanAll(String key, String cursor, String pattern, Integer limit) { try(Jedis jedis = iRedisFactory.getConnection()) { ScanParams scanParams = new ScanParams(); scanParams.match(pattern); scanParams.count(limit); List<String> resultList = new LinkedList<>(); while (true) { ScanResult scanResult = jedis.zscan(key,cursor,scanParams); cursor = scanResult.getCursor(); if (ScanParams.SCAN_POINTER_START.equals(cursor)){ break; } resultList.addAll(scanResult.getResult()); } return resultList; }catch (Exception e){ log.error("jedis sscanAll has error, error is ", e); } return null; } 复制代码
这是一个用于有序集合使用的遍历指令
\
在Centos操作系统上边执行这些指令的案例:
scan指令
redis 127.0.0.1:6379> scan 0 MATCH *11* 1) "288" 2) 1) "key:911" redis 127.0.0.1:6379> scan 288 MATCH *11* 1) "224" 2) (empty list or set) redis 127.0.0.1:6379> scan 224 MATCH *11* 1) "80" 2) (empty list or set) redis 127.0.0.1:6379> scan 80 MATCH *11* 1) "176" 2) (empty list or set) redis 127.0.0.1:6379> scan 176 MATCH *11* COUNT 1000 1) "0" 2) 1) "key:611" 2) "key:711" 3) "key:118" 4) "key:117" 5) "key:311" 6) "key:112" 7) "key:111" 8) "key:110" 9) "key:113" 10) "key:211" 11) "key:411" 12) "key:115" 13) "key:116" 14) "key:114" 15) "key:119" 16) "key:811" 17) "key:511" 18) "key:11" 复制代码
sscan指令
> sscan vip-info-set 0 count 10 0 [[1001]] zscan指令 > zscan review:222 0 match * count 10 0 idea 1 idea2 2 idea3 3 idea4 4 复制代码
hscan指令
> hscan user-map 0 match * count 10 0 1 user1 2 user2 3 user3 复制代码
SCAN的遍历顺序
关于scan命令的遍历顺序,我们可以具体看一下。
> keys * user test-key-1 test-key-3 test-key-4 test-key-2 > scan 0 match * count 1 2 test-key-3 > scan 2 match * count 1 6 user > scan 6 match * count 1 1 test-key-4 > scan 1 match * count 1 7 test-key-1 > scan 7 match * count 1 0 test-key-2 复制代码
我们的Redis中有3个key,我们每次只遍历一个一维数组中的元素。如上所示,SCAN命令的遍历顺序是
0->2->6->1->7->0 复制代码
这个顺序看起来有些奇怪。我们把它转换成二进制就好理解一些了。我们发现每次这个序列是高位加1的。普通二进制的加法,是从右往左相加、进位。而这个序列是从左往右相加、进位的。
000->010->110->001->111->000 复制代码
那么为什么redis的作者想要这么设计呢?
这个地方涉及到了redis的rehash操作,假设我们原先的索引为:
000->100->010->110->001->101->011->111 复制代码
那么在进行了一轮扩容之后,索引会变为:
0000->1000->0100->1100->0010->1010->0110->1110->0001->1001->1101->1111 复制代码
原来挂接在xxx下的所有元素被分配到0xxx和1xxx下。当我们即将遍历010时,dict进行了rehash,这时,scan命令会从0100开始遍历,而000和100(原00下挂接的元素)不会再被重复遍历。
再来看看缩容的情况。假设dict从4位缩容到3位,当即将遍历1110时,dict发生了缩容,这时scan会遍历011。这时011下的元素会被重复遍历,但011之前的元素都不会被重复遍历了。所以,缩容时还是可能会有些重复元素出现的。因此在使用scan指令的时候,时而会出现重复的元素。