分析版本:redis.clients:jedis:2.9.0
异常信息抛出的位置位于redis.clients.util.Pool.getResource
public T getResource() {
try {
return internalPool.borrowObject();
} catch (NoSuchElementException nse) {
throw new JedisException("Could not get a resource from the pool", nse);
} catch (Exception e) {
throw new JedisConnectionException("Could not get a resource from the pool", e);
}
}
redis.clients.util.Pool.getResource
会从 JedisPool 实例池中返回一个可用的 redis 连接。分析源码可知 JedisPool 继承了 redis.clients.util.Pool
,而这个 Pool 是通过 commons-pool 开源工具包中的org.apache.commons.pool2.impl.GenericObjectPool
来实现对 Jedis 实例的管理的。所以我们可以先分析一下 GenericObjectPool。
public class JedisPool extends Pool<Jedis> {}
public abstract class Pool<T> implements Closeable {
protected GenericObjectPool<T> internalPool;
}
其中GenericObjectPool三个重要个几个属性是:
// 最大连接数
private volatile int maxIdle = GenericObjectPoolConfig.DEFAULT_MAX_IDLE;//8
// 最大空闲数
private volatile int minIdle = GenericObjectPoolConfig.DEFAULT_MIN_IDLE;//0
// 最大等待时间,单位毫秒(million seconds)
private volatile long maxWaitMillis = BaseObjectPoolConfig.DEFAULT_MAX_WAIT_MILLIS;//-1
参数之间的关系为:连接池中无可用连接时会会进行等待maxWait时间,若超出泽抛Could not get a resource from the pool
异常。所以应根据程序实际情况合理设置这三个参数的值,尽量避免这个异常。
如果连接池没有可用 Jedis 连接,会等待 maxWaitMillis(毫秒),依然没有获取到可用 Jedis 连接,会抛出这个异常:redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
验证:JedisPool 默认的maxTotal=8
,下面的代码从 JedisPool 中借了8次 Jedis,但是没有归还,当第9次(jedisPool.getResource().ping())
时,就会抛出异常:
public static void main(String[] args) {
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxWaitMillis(1000);
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379, 10, "******");
//向JedisPool借用8次连接,但是没有执行归还操作。
for (int i = 0; i < 8; i++) {
Jedis jedis;
try {
jedis = jedisPool.getResource();
String ping = jedis.ping();
System.out.println(ping);
} catch (Exception e) {
System.out.println(e);
}
}
System.out.println("向JedisPool借用8次连接完成");
String ping = jedisPool.getResource().ping();
System.out.println(ping);
}
所以推荐使用的代码规范是:
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//具体的命令
jedis.executeCommand()
} catch (Exception e) {
//如果命令有key最好把key也在错误日志打印出来,对于集群版来说通过key可以帮助定位到具体节点。
logger.error(e.getMessage(), e);
} finally {
//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
if (jedis != null) {
jedis.close();
}
}
示例:一次命令时间(borrow|return resource + Jedis执行命令(含网络) )
的平均耗时约为 1ms,一个连接的QPS大约是1000,业务期望的 QPS 是 80000,那么理论上需要的资源池大小是 80000 / 1000 = 80个,实际 maxTotal 可以根据理论值合理进行微调。
调节 maxTotal 值的时候,需要考虑对应服务的机器配置,可参考 JedisPool资源池优化。
示例:Redis发生了阻塞(例如慢查询等原因),所有连接在超时时间范围内等待,并发量较大时,会造成连接池资源不足。
避免一些 big key 的查询,批量获取数据的时候,记得分页。
示例:丢包、DNS、客户端TCP参数配置等,可参考 Jedis常见问题分析