问题分析
首先,先看报错:
java.lang.Exception: Apparent connection leak detected
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128) ~[HikariCP-3.4.5.jar:?]
at com.skyline.MyTest.getConnection(MyTest.java:36)
at java.lang.Thread.run(Thread.java:748) [?:1.8.0_261]
Apparent connection leak detected
,网上很多解释都说是连接泄露,但是这个报错就等于连接泄漏了吗?先说答案:并不是!!!
首先,这个报错是从ProxyLeakTask
中抛出来的,源码不多,先贴出来
class ProxyLeakTask implements Runnable
{
private static final Logger LOGGER = LoggerFactory.getLogger(ProxyLeakTask.class);
static final ProxyLeakTask NO_LEAK;
//调度器
private ScheduledFuture<?> scheduledFuture;
private String connectionName;
//异常对象
private Exception exception;
//线程名
private String threadName;
//是否泄露
private boolean isLeaked;
static
{
//创建一个不检测连接泄漏的task
NO_LEAK = new ProxyLeakTask() {
@Override
void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold) {}
@Override
public void run() {}
@Override
public void cancel() {}
};
}
//构造器,需要传入连接池中的连接对象
ProxyLeakTask(final PoolEntry poolEntry)
{
//※重点来了,这时创建一个异常对象,当ProxyLeakTask被new出来的时候,就是连接被业务代码申请的时候
this.exception = new Exception("Apparent connection leak detected");
this.threadName = Thread.currentThread().getName();
this.connectionName = poolEntry.connection.toString();
}
private ProxyLeakTask()
{
}
//※很重要,开始调度到倒计时开始
void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold)
{
//拿到这个返回值是为了将来可以cancel这个调度
scheduledFuture = executorService.schedule(this, leakDetectionThreshold, TimeUnit.MILLISECONDS);
}
/** {@inheritDoc} */
@Override
public void run()
{
//当代码执行到这里时,说明上面的调度没有被cancel,
//那么Hikari就认为之前申请的连接可能已经泄露了,
//这个run方法是由executorService回调的
isLeaked = true;
final StackTraceElement[] stackTrace = exception.getStackTrace();
final StackTraceElement[] trace = new StackTraceElement[stackTrace.length - 5];
System.arraycopy(stackTrace, 5, trace, 0, trace.length);
exception.setStackTrace(trace);
LOGGER.warn("Connection leak detection triggered for {} on thread {}, stack trace follows", connectionName, threadName, exception);
}
//取消调度,这个方法会在连接被释放时回调
void cancel()
{
//先把这个调度任务取消
scheduledFuture.cancel(false);
if (isLeaked) {
//很遗憾,如果之前调度任务已经走过了,那么就再打个日志说明一下
//之前那个被认为已经泄露了的连接会到池子里了
LOGGER.info("Previously reported leaked connection {} on thread {} was returned to the pool (unleaked)", connectionName, threadName);
}
}
}
接下来我们看下ProxyLeakTask是什么时候被创建的,首先,我们来到这个方法:com.zaxxer.hikari.pool.HikariPool#getConnection(long)
这个是从Hikari中获取连接的方法,大家都知道,但凡你要操作数据库,必然先取连接。我们重点关注一下这个方法 poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
一层一层的看,我们先看这个leakTaskFactory.schedule
方法:
ProxyLeakTask schedule(final PoolEntry poolEntry)
{
//先判断leakDetectionThreshold是不是等于0,
//如果等于0,返回最一开始那个不检查连接泄露的Task,
//如果不等于0,就通过scheduleNewTask创建一下
return (leakDetectionThreshold == 0) ? ProxyLeakTask.NO_LEAK : scheduleNewTask(poolEntry);
}
重头戏scheduleNewTask
来了
private ProxyLeakTask scheduleNewTask(PoolEntry poolEntry) {
//创建一个task
ProxyLeakTask task = new ProxyLeakTask(poolEntry);
//开始调度,※注意,这里就开始倒计时了!!!!
task.schedule(executorService, leakDetectionThreshold);
return task;
}
那么poolEntry.createProxyConnection
返回的是个什么对象呢?答案是com.zaxxer.hikari.pool.ProxyConnection
这里肯定是代理了。我们重点关注一下连接释放的方法吧:
看到没有,当连接被释放时,会回调leakTask的cancel方法。
总结
leakDetectionThreshold
如果等于0,那么Hikari就不会做连接泄露的检查leakDetectionThreshold
的时间=从获取调用getConnection()
开始到调用connection.close()
的时间,注意,如果从数据库中获取真正的连接的耗时也是计算在内的!Apparent connection leak detected
并不等于连接泄露,我理解只能是猜测可能出现了连接泄露,如果日志中可以找到成对的Previously reported leaked connection {} on thread {} was returned to the pool (unleaked)
那么可以确定的是,连接并没有泄露,只是连接的使用时长>leakDetectionThreshold
了,虽然不是连接泄露,但是可能是执行慢的SQL,也是需要进一步跟踪排查的。- 这个连接泄露的检查机制真的很厉害,从来没想过调度任务可以这么玩,确实有意思,打开了新世界的大门。
引申一下,leakDetectionThreshold的配置项在spring.datasource.hikari.leak-detection-threshold
单位是毫秒