springcloud-redistemplate-游标scan使用注意事项

业务场景

一次性取出redis一个field中的所有key,并遍历。

方案一

使用redisTemplate.opsForHash().keys("filed")

前期数据量少,未感知到性能问题。后查询资料得知,数据量上去后keys方法严重消耗CPU,一般在生产环境禁用keys方法。

防患于未然,该方式摒弃!

方案二

使用redisTemplate游标分批次获取

使用scan主要两个参数:match和count。

match:key的正则表达式

count:每次扫描的记录数。值越小,扫描次数越过、越耗时。建议设置在1000-10000

public void getKeysTest(){
        try {
            Cursor>> cursor = deviceRedis.opsForHash().scan("filed", 
                    ScanOptions.scanOptions().match("*").count(1000).build());
            while (cursor.hasNext()) {
                String key = cursor.next().getKey()
                Set valueSet = cursor.next().getValue();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

使用scan代替keys肯定会导致整个查询消耗的总时间变大,但不会影响redis服务卡顿,影响服务使用。


后记

使用scan代替keys方式后,发现仅测试环境如此低的并发和数据量情况下,redis却经常会报错:

Could not get a resource from the pool !重启后回复正常,然后又重复!

问题定位

代码中对于redis的存、取、删除、过期设置、游标等操作都有涉及,最开始始终无法定位具体哪里的原因。

以为是RedisTemplate从JedisPool中获取连接使用后没有释放链接造成的。

但RedisTemplate是对Jedis做了二次封装,可自动通过连接池来管理连接。解读RedisTemplate源码后,确认了这一点:

springcloud-redistemplate-游标scan使用注意事项_第1张图片

RedisTemplate封装的redis操作方法,大多方法最底层都会调用最核心的excute方法,在excute方法的finally模块可以看到每次操作完成后都会自动关闭释放连接。该原因剔除!

最后通过调低连接池的max-active,多次的测试、重现,最终确认问题的原因是由于调用了scan后redis连接数只升不降。

通过redis-cli进入redis控制台,使用CLIENT LIST命令查看redis客户端连接信息:

springcloud-redistemplate-游标scan使用注意事项_第2张图片

其中,age标识已建立连接的时长(单位:秒),cmd标识操作命令。

正常情况下,redis连接信息中99%只应显示ping和auth命令,因为其他redis操作都是操作完成后立即释放连接,但是从上图看到scan命令对应连接的连接时长远高于其他操作,说明连接一直未断开。后来又测试几次,发现scan操作的连接信息只升不降。

最终问题终于定位le:scan操作后,连接没有释放,导致连接池可用连接被用完!

解决方式

又查看到scan的源码:

@Override
	public Cursor> scan(K key, final ScanOptions options) {

		final byte[] rawKey = rawKey(key);
		return template.executeWithStickyConnection(new RedisCallback>>() {

			@Override
			public Cursor> doInRedis(RedisConnection connection) throws DataAccessException {

				return new ConvertingCursor, Map.Entry>(connection.hScan(rawKey, options),
						new Converter, Map.Entry>() {

							@Override
							public Entry convert(final Entry source) {

								return new Map.Entry() {

									@Override
									public HK getKey() {
										return deserializeHashKey(source.getKey());
									}

									@Override
									public HV getValue() {
										return deserializeHashValue(source.getValue());
									}

									@Override
									public HV setValue(HV value) {
										throw new UnsupportedOperationException("Values cannot be set when scanning through entries.");
									}
								};

							}
						});
			}

		});

没有看到操作完成后关闭/释放/归还连接(或者我没找到吧)。

只能通过手动关闭来实现:

public void getKeysTest(){
        try {
            Cursor>> cursor = deviceRedis.opsForHash().scan("filed", 
                    ScanOptions.scanOptions().match("*").count(1000).build());
            while (cursor.hasNext()) {
                String key = cursor.next().getKey()
                Set valueSet = cursor.next().getValue();
            }
            //关闭scan
            cursor.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

最后问题解决了,未再出现Could not get a resource from the pool!错误。

与各位共勉!

你可能感兴趣的:(Redis)