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

本文介绍了在SpringCloud中使用RedisTemplate的Scan方法替代keys命令处理大量数据的场景,以避免性能问题。在实际应用中遇到Redis连接池资源耗尽的问题,经过排查发现是Scan操作未正确关闭游标导致。解决方案是手动关闭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<Map.Entry<String, Set<String>>> cursor = deviceRedis.opsForHash().scan("filed", 
                    ScanOptions.scanOptions().match("*").count(1000).build());
            while (cursor.hasNext()) {
                String key = cursor.next().getKey()
                Set<String> 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源码后,确认了这一点:

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

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

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

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

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

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

解决方式

又查看到scan的源码:

@Override
	public Cursor<Entry<HK, HV>> scan(K key, final ScanOptions options) {

		final byte[] rawKey = rawKey(key);
		return template.executeWithStickyConnection(new RedisCallback<Cursor<Map.Entry<HK, HV>>>() {

			@Override
			public Cursor<Entry<HK, HV>> doInRedis(RedisConnection connection) throws DataAccessException {

				return new ConvertingCursor<Map.Entry<byte[], byte[]>, Map.Entry<HK, HV>>(connection.hScan(rawKey, options),
						new Converter<Map.Entry<byte[], byte[]>, Map.Entry<HK, HV>>() {

							@Override
							public Entry<HK, HV> convert(final Entry<byte[], byte[]> source) {

								return new Map.Entry<HK, HV>() {

									@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<Map.Entry<String, Set<String>>> cursor = deviceRedis.opsForHash().scan("filed", 
                    ScanOptions.scanOptions().match("*").count(1000).build());
            while (cursor.hasNext()) {
                String key = cursor.next().getKey()
                Set<String> valueSet = cursor.next().getValue();
            }
            //关闭scan
            cursor.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

与各位共勉!

评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值