测试一个例子
public static void main(String[] args) throws Exception{ DataSource dataSource = dataSource(); for (int i =0 ;i< 5;i++){ dataSource.getConnection(); } Connection connection = dataSource.getConnection(); } public static DataSource dataSource(){ DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/ds0?serverTimezone=UTC"); dataSource.setPassword("123456"); dataSource.setUsername("root"); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setInitialSize(2); dataSource.setMaxActive(5); dataSource.setMaxWait(30000); return dataSource; }
使用druid连接池设置最大连接数为5,获取连接超时时间为30S。上边的例子很显然在30S之后会抛出获取
连接超时的异常。因为我们已经从池子里拿了5个连接并没有归还,druid在等待超时时间后获取不到可用
的连接了已经。如果时代码中某些地方忘记调用connection.close()方法,在多次执行后连接池中所有的连接
都会处于活动状态,连接池将被耗尽无法再获取新的连接,而这些活动连接实际又是空闲的连接并没有在工作,
这样就出现了连接泄露,druid是否可以回收掉可能泄露的连接呢?我们对连接池添加几个参数
dataSource.setRemoveAbandoned(true);//是否回收泄露的连接,默认为false dataSource.setTimeBetweenEvictionRunsMillis(10000);//destroy线程执行回收回收任务的间隔 dataSource.setRemoveAbandonedTimeoutMillis(10000);//连接回收的超时时间
这样配置后,上述demo就能拿到连接,因为前边没有回收的连接已经被当做泄露的连接回收了。
代码执行逻辑:
public class DestroyConnectionThread extends Thread { public DestroyConnectionThread(String name){ super(name); this.setDaemon(true); } public void run() { initedLatch.countDown(); for (;;) { // 从前面开始删除 try { if (closed) { break; } //如果设置了这个参数 等待直到预期的时间后再执行任务 默认为 1000ms if (timeBetweenEvictionRunsMillis > 0) { Thread.sleep(timeBetweenEvictionRunsMillis); } else { Thread.sleep(1000); // } if (Thread.interrupted()) { break; } //真正执行回收的逻辑 destroyTask.run(); } catch (InterruptedException e) { break; } } } }
进入真正的回收逻辑:
public class DestroyTask implements Runnable { public DestroyTask() { } @Override public void run() {
//该方法 主要是1.对于超过phyTimeoutMillis的空闲连接进行强制回收,不过此时空闲连接数是否满足连接池最小的连接数。
2.如果此空闲连接空闲时间大于最小空闲时间,但是连接数大于最小连接数将此连接回收
3.如果此空闲连接空闲时间大于最大空闲时间,将此连接回收
4.剩余的空闲连接进行连接有效性的检测,如果该连接失效将回收此连接。
5.池子的数量小于最小的连接数时补充新的连接。
//4 5步骤需要keepAlive参数为true
shrink(true, keepAlive); if (isRemoveAbandoned()) {
//对处于活动的连接进行回收,连接池的连接的总数量为 poolingCount+ActiveCount 本方法就是对activeConnections进行回收 removeAbandoned(); } } }
我们主要看removeAbandoned方法怎么处理可能泄露的连接:
int removeCount = 0; long currrentNanos = System.nanoTime(); List<DruidPooledConnection> abandonedList = new ArrayList<DruidPooledConnection>(); activeConnectionLock.lock(); try { Iterator<DruidPooledConnection> iter = activeConnections.keySet().iterator(); for (; iter.hasNext();) { DruidPooledConnection pooledConnection = iter.next(); if (pooledConnection.isRunning()) { continue; } long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000); if (timeMillis >= removeAbandonedTimeoutMillis) { iter.remove(); pooledConnection.setTraceEnable(false); abandonedList.add(pooledConnection); } } } finally { activeConnectionLock.unlock(); } if (abandonedList.size() > 0) { for (DruidPooledConnection pooledConnection : abandonedList) { final ReentrantLock lock = pooledConnection.lock; lock.lock(); try { if (pooledConnection.isDisable()) { continue; } } finally { lock.unlock(); } JdbcUtils.close(pooledConnection); pooledConnection.abandond(); removeAbandonedCount++; removeCount++; if (isLogAbandoned()) { StringBuilder buf = new StringBuilder(); buf.append("abandon connection, owner thread: "); buf.append(pooledConnection.getOwnerThread().getName()); buf.append(", connected at : "); buf.append(pooledConnection.getConnectedTimeMillis()); buf.append(", open stackTrace\n"); StackTraceElement[] trace = pooledConnection.getConnectStackTrace(); for (int i = 0; i < trace.length; i++) { buf.append("\tat "); buf.append(trace[i].toString()); buf.append("\n"); } buf.append("ownerThread current state is " + pooledConnection.getOwnerThread().getState() + ", current stackTrace\n"); trace = pooledConnection.getOwnerThread().getStackTrace(); for (int i = 0; i < trace.length; i++) { buf.append("\tat "); buf.append(trace[i].toString()); buf.append("\n"); } LOG.error(buf.toString()); } } } return removeCount;
首先判断当前连接是否处于running状态,如果处于running状态则跳过。什么是running状态?
进入connection内部我们可以看到两个方法:
final void beforeExecute() { final DruidConnectionHolder holder = this.holder; if (holder != null && holder.dataSource.removeAbandoned) { running = true; } } final void afterExecute() { final DruidConnectionHolder holder = this.holder; if (holder != null) { DruidAbstractDataSource dataSource = holder.dataSource; if (dataSource.removeAbandoned) { running = false; holder.lastActiveTimeMillis = System.currentTimeMillis(); } dataSource.onFatalError = false; } }
在执行beforeExecute时,如果打开了removeAbandoned,则会将此连接设置为running,标志当前
连接正在工作。beforeExecute方法会在使用DruidPooledPreparedStatement和DruidPooledStatement
之中进行query、update等时调用,此时这个连接正在与数据库打交道,肯定不能被回收的。
afterExecute会在这些数据库操作执行后执行,如果此时 removeAbandoned属性打开了,则会将
running状态设置为false,标志当前连接不处于正在运行的状态并且记录当前的时间为上一次活跃时间。
接下来removeAbandoned方法中就会将这些超过removeAbandonedTimeoutMillis的连接进行回收,并且
标志为已经丢弃的状态。
druid通过上述步骤对可能发生泄露的连接进行了回收,可以防止连接泄露的情况下程序无法获取新的连接。
但是也有风险,如果程序上使用完连接并没有进行归还,在下一次任务执行时又使用此连接进行操作,就
很可能会发生 connection closed的异常。