问题追踪
我使用的Jedis版本是2.9.0,通过连接池访问,在做压力测试时,发现在高并发环境下,连接泄露,连接池内的连接未能正常返还,导致"could not get resource fron pool".
看使用代码:
public void execute(RedisAction callback) {
Jedis jedis = jedisPool.getResource();
if (jedis == null) {
throw new RuntimeException("fetch jedis connection failed ");
}
try {
callback.doInRedis(jedis);
} finally {
jedis.close();
}
}
使用规范的try-finally方式,使用完毕后归还jedis连接,但是连接仍然泄露,在平稳运行一段时间后应用越来越慢,最后无法获取连接。
我推测是在Jedis归还过程中,由于高并发而导致的连接池泄露,看源码:
@Override
public void close() {
if (dataSource != null) {
if (client.isBroken()) {
this.dataSource.returnBrokenResource(this);
} else {
this.dataSource.returnResource(this);
}
this.dataSource = null;
} else {
super.close();
}
}
发现:在jedis.close()的过程中,先将jedis归还(this.dataSource.returnResource(this)),再将dataSource设置为null. 问题就在这里了!
在高并发情况下,jedis被归还后的一瞬间即可能被其他线程借去,此时,上一次的close还没有完全执行完,在此时再执行下面的this.dataSource = null 这句,会导致下一个borrow到jedis连接的线程再也无法将连接归还给连接池,而只能走到super.close()将连接关闭。
解决方式
1. 翻github看到官方已经记录并且在2.10.2版本修复了这个问题, 只要将jedis版本升级到2.10.2以上就可以解决这个问题了,附上2.10.2的jedis.close()源码:
@Override
public void close() {
if (dataSource != null) {
Pool<Jedis]]> pool = this.dataSource;
this.dataSource = null;
if (client.isBroken()) {
pool.returnBrokenResource(this);
} else {
pool.returnResource(this);
}
} else {
super.close();
}
}
先设置dataSource为null,再归还Jedis连接, 就不会再有连接泄露了。
2. 某些情况下,可能无法直接这么简单地做jedis版本替换。
我一直有一个疑惑,那就是spring boot 2.1.x其实默认依赖的都是jedis2.9.x的版本,为什么使用springboot的时候不会出现jedis连接泄露呢?
我尝试看了spring-data-redis的源码,并没有找到什么特别的处理,工作时间太紧张,没时间追究,只得自己动手fix,当使用2.9.x版本的jedis时,避免连接泄露,在返还连接的时候,这么写:
@SuppressWarnings("deprecation")
public void closeJedis(Jedis jedis) {
if (null != jedis) {
if (jedisPool != null) {
jedis.setDataSource(null);
if (jedis.getClient().isBroken()) {
jedisPool.returnBrokenResource(jedis);
} else {
jedisPool.returnResource(jedis);
}
} else {
jedis.getClient().close();
}
}
}
经过测试可解决问题。
关于spring到底做了什么解决了2.9.x版本的这个问题,有谁知道可以告诉我?网上真的查不到,自己看也看不出。