在生产环境中使用了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指令的时候,时而会出现重复的元素。