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