redis用scan代替keys
众所周知,当redis中key数量越大,keys 命令执行越慢,而且最重要的会阻塞服务器,对单线程的redis来说,简直是灾难,且在生产环境,keys命令一般是被禁止的。scan可用来替换keys请求。
# scan用法
SCAN cursor [MATCH pattern] [COUNT count]
scan是一个增量迭代式的命令,这意味着每次调用这个命令都会返回一个游标cursor,该游标用于下次查询。查询开始时,cursor值为0;当查询结束时,cursor的值也回归到0。
举个例子:
# 开始查询,scan cursor为0,返回的cursor为17
redis 127.0.0.1:6379> scan 0
1) "17"
2) 1) "key:12"
2) "key:8"
3) "key:4"
4) "key:14"
5) "key:16"
6) "key:17"
7) "key:15"
8) "key:10"
9) "key:3"
10) "key:7"
11) "key:1"
# 下一次查询,以上一次查询返回的cursor为起始位置
redis 127.0.0.1:6379> scan 17
# 查询返回cursor为0,标志查询结束
1) "0"
2) 1) "key:5"
2) "key:18"
3) "key:0"
4) "key:2"
5) "key:19"
6) "key:13"
7) "key:6"
8) "key:9"
9) "key:11"
count
count可理解为迭代过程中的步长,指每次调用scan时应执行的工作量,该值默认为10。每次调用count的值可以随意指定,只要下一次传递cursor是上一次调用返回的cursor就行。
match
需要注意的是,match操作时在元素被检出后执行的。假设redis中只有少量元素符合pattern条件,那么很可能在多次调用中scan返回的数据为空,例如:
# 查找key中包含11的键,因为这里没有指定count,所以默认为10
redis 127.0.0.1:6379> scan 0 MATCH *11*
1) "288"
2) 1) "key:911"
# 在这次调用中,count为10,起始cursor为288,返回的结果中并没有满足*11*条件的key
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)
# count指定为1000,找到了。
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"
redis 127.0.0.1:6379>
scan的优缺点
可以看出,Redis的SCAN操作由于其整体的数据设计,无法提供特别准的scan操作,仅仅是一个“can ‘ t guarantee , just do my best”的实现:
- 提供键空间的遍历操作,支持游标,复杂度O(1), 整体遍历一遍只需要O(N);
- 提供结果模式匹配;
- 支持一次返回的数据条数设置,但仅仅是个hints,有时候返回的会多;
- 弱状态,所有状态只需要客户端需要维护一个游标;
- 无法提供完整的快照遍历,也就是中间如果有数据修改,可能有些涉及改动的数据遍历不到;
- 每次返回的数据条数不一定,极度依赖内部实现;
- 返回的数据可能有重复,应用层必须能够处理重入逻辑;上面的示例代码中,redisTemplate.execute方法是个Set,相当于已经对于返回的key去重
- count是每次扫描的key个数,并不是结果集个数。count要根据扫描数据量大小而定,Scan虽然无锁,但是也不能保证在超过百万数据量级别搜索效率;count不能太小,网络交互会变多,count要尽可能的大。在搜索结果集1万以内,建议直接设置为与所搜集大小相同
spring中使用scan实现keys
/**
* 以count为步长查找符合pattern条件的keys
*
* @param redisTemplate 指定redis
* @param pattern 匹配条件
* @param count 一次在count条记录中match符合pattern条件的记录。若count<=0,使用1000
* @return Set<String> 若limit<= 0,返回所有;否则返回查找结果
*/
public Set<String> scanKeys(RedisTemplate<String, Object> redisTemplate, String pattern, int count) {
log.info("pattern:{}, count:{}", pattern, count);
return redisTemplate.execute(new RedisCallback<Set<String>>() {
@Override
public Set<String> doInRedis(RedisConnection connection) throws DataAccessException {
Set<String> tmpKeys = new HashSet<>();
ScanOptions options;
if (count <= 0) {
options = ScanOptions.scanOptions().match(pattern).count(1000).build();
} else {
options = ScanOptions.scanOptions().match(pattern).count(count).build();
}
// 迭代一直查找,直到找到redis中所有满足条件的key为止(cursor变为0为止)
Cursor<byte[]> cursor = connection.scan(options);
while (cursor.hasNext()) {
tmpKeys.add(new String(cursor.next()));
}
return tmpKeys;
}
});
}
Jedis使用scan实现keys
/**
* 扫描keys方法,替代Keys接口
* @param jedis
* @param keyPattern
* @return
*/
public static Set<String> scanKeys(Jedis jedis, String keyPattern) {
Set<String> keys = new HashSet<>();
String cursor = ScanParams.SCAN_POINTER_START;
ScanParams sp = new ScanParams();
sp.match(keyPattern);
sp.count(1000);
do{
ScanResult<String> ret = jedis.scan(cursor, sp);
List<String> result = ret.getResult();
if(result!=null && result.size() > 0){
keys.addAll(result);
}
//再处理cursor
cursor = ret.getCursor();
// 迭代一直到cursor变为0为止
}while(!cursor.equals(ScanParams.SCAN_POINTER_START));
return keys;
}
参考链接
https://redis.io/commands/scan