项目场景:
今日看MallChat业务代码中的RedisUtil工具类,看到没见过的方法,记录一下。
ScanOptions
平时操作Redis都是使用redisTemplate等操作。今天看到使用ScanOptions操作便查询了一下这个东西的使用场景。正常查询redis一些key的时候之前一般都用Keys * 这个方法去查询符合条件的key 。但是这个命令影响性能。如果生产环境的key很多,那么keys * 会很慢,因为redis是单线程的,这个keys 命令会导致redis卡顿,所以一般来说keys 命令都是禁用的。 所以redis后面出现了scan这个命令。这个命令类似分页查询,一次不会把全部key返回给你,而是返回一个游标Cursor。游标从0开始遍历,再回到0便利结束。
命令格式:
SCAN cursor [MATCH pattern] [COUNT count]
下面贴上RedisUtil里面的scan方法业务代码
/**
* 查找匹配key
*
* @param pattern key
* @return /
*/
public static List<String> scan(String pattern) {
ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
Cursor<byte[]> cursor = rc.scan(options);
List<String> result = new ArrayList<>();
while (cursor.hasNext()) {
result.add(new String(cursor.next()));
}
try {
RedisConnectionUtils.releaseConnection(rc, factory);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return result;
}
可以看到使用ScanOptions.scanOptions()创建builder,然后将表达式传入match方法。在调用build方法获得ScanOptions。然后通过链接工厂获得Redis的Connection。核心就是调用RedisConnection.scan()方法执行scan命令,然后返回游标cursor 。然后固定写法迭代器判断下一个还有没有值,然后取出就行。最后try catch释放连接。
原因分析:
这种写法在数据量小的时候没有问题,但是在key很多的时候就会出现卡死的问题。原因是scan是一个基于游标的迭代器,它的时间复杂度为O(n)相比于Keys命令来说有一个优点是不会阻塞redis,但在某条语句中如果不是异步调用的话会阻塞我们的程序执行,相当于分页查找redis中的所有key每拿出一页才会再次去拿下一页,spring内置的jedis默认页面大小是10。当你不用count()指定页面大小时,一页默认10条,当key数量大时就要频繁的通讯。
解决方案:
因此当数据量大时就要指定页面大小,防止多次通讯
ScanOptions options = ScanOptions.scanOptions().match(pattern).count(1000).build();