DBCP连接池耗尽问题

问题描述

使用DBCP连接池。每当访问数据库超时,永久占用数据库连接池的一个连接数。多次超时发生后,数据库连接池耗尽,后续的数据库请求拿不到数据库连接,全部hang住。

代码分析

1 数据库访问获取连接

DBCP连接池构建了GenericObjectPool,通过GenericObjectPool来管理数据库连接。当数据库访问发生时,通过GenericObjectPool.borrowObject()方法获取一个数据库连接对象。

当存在空闲连接或者活跃连接数没达到maxAtive上限,则成功分配一个连接,numActive(活跃连接数)计数加1。代码如下:

            // activate & validate the object
            try {
                _factory.activateObject(latch.getPair().value);
                if(_testOnBorrow &&
                        !_factory.validateObject(latch.getPair().value)) {
                    throw new Exception("ValidateObject failed");
                }
                synchronized(this) {
                    _numInternalProcessing--;
                    _numActive++;
                }
                return latch.getPair().value;
            }

如果numActive(活跃连接数) + numInternalProcessing(正在申请连接的请求数)达到maxAtive上限,则认为连接耗尽。这时候,会根据耗尽策略执行不同的操作,默认是BLOCK,在maxWait不大于0的情况下,永久等待,直到有数据库连接释放并调用latch.notify()。

            // If no object was allocated from the pool above
            if(latch.getPair() == null) {
                // check if we were allowed to create one
                if(latch.mayCreate()) {
                    // allow new object to be created
                } else {
                    // the pool is exhausted
                    switch(whenExhaustedAction) {
                        case WHEN_EXHAUSTED_BLOCK:
                            try {
                                synchronized (latch) {
                                    // Before we wait, make sure another thread didn't allocate us an object
                                    // or permit a new object to be created
                                    if (latch.getPair() == null && !latch.mayCreate()) {
                                        if(maxWait <= 0) {
                                            latch.wait();
                                        } else {
                                            // this code may be executed again after a notify then continue cycle
                                            // so, need to calculate the amount of time to wait
                                            final long elapsed = (System.currentTimeMillis() - starttime);
                                            final long waitTime = maxWait - elapsed;
                                            if (waitTime > 0)
                                            {
                                                latch.wait(waitTime);
                                            }
                                        }
                                    } else {
                                        break;
                                    }
                                }
                            } 

hang住的线程就是block在这里,说明数据库连接池已经耗尽了(numActive + numInternalProcessing >= maxAtive)。

那么,数据库连接为什么会耗尽呢?我们debug一下代码,在数据库访问超时的时候,看看到底发生了什么。

 

2 数据库访问超时释放连接

通过异常堆栈,跟踪到SqlMapClientTemplate。超时异常是在try块中发生的,并被catch住进行了翻译,释放连接的逻辑在finally代码块中。所以,理论上无论是否发生异常,都会在finally代码块中调用DataSourceUtils.doReleaseConnection释放连接。

            // Execute given callback...
            try {
                return action.doInSqlMapClient(session);
            }
            catch (SQLException ex) {
                throw getExceptionTranslator().translate("SqlMapClient operation", null, ex);
            }
            finally {
                try {
                    if (springCon != null) {
                        if (transactionAware) {
                            springCon.close();
                        }
                        else {
                            DataSourceUtils.doReleaseConnection(springCon, dataSource);
                        }
                    }
                }
                catch (Throwable ex) {
                    logger.debug("Could not close JDBC Connection", ex);
                }
            }

那么,会不会是在释放连接的时候发生了问题,没有将连接归还给连接池?

在debug模式下,我们找到了答案(debug模式下有更多的异常堆栈)。释放连接的时候,的确发生了异常。由于数据库访问超时,底层连接已经关闭,抛出了“Already closed”异常。

看一下PoolableConnection.close的代码逻辑:

     public synchronized void close() throws SQLException {
        boolean isClosed = false;
        try {
            isClosed = isClosed();
        } catch (SQLException e) {
            try {
                _pool.invalidateObject(this);
            } catch (Exception ie) {
                // DO NOTHING the original exception will be rethrown
            }
            throw new SQLNestedException("Cannot close connection (isClosed check failed)", e);
        }
        if (isClosed) {
            throw new SQLException("Already closed.");
        } else {
            try {
                _pool.returnObject(this);
            } catch(SQLException e) {
                throw e;
            } catch(RuntimeException e) {
                throw e;
            } catch(Exception e) {
                throw new SQLNestedException("Cannot close connection (return to pool failed)", e);
            }
        }
    }

如果底层连接已经关闭,isClosed条件为true,那么直接抛出“Already closed”的异常,没有执行任何其他操作(比如:将连接归还,活跃连接数减1)。

归还连接需要调用GenericObjectPool.returnObject:

    public void returnObject(Object obj) throws Exception {
        try {
            addObjectToPool(obj, true);
        }
        ...
    }

    private void addObjectToPool(Object obj, boolean decrementNumActive) throws Exception {
        ...
        // Add instance to pool if there is room and it has passed validation
        // (if testOnreturn is set)
        synchronized (this) {
            if (isClosed()) {
                shouldDestroy = true;
            } else {
                if((_maxIdle >= 0) && (_pool.size() >= _maxIdle)) {
                    shouldDestroy = true;
                } else if(success) {
                    // borrowObject always takes the first element from the queue,
                    // so for LIFO, push on top, FIFO add to end
                    if (_lifo) {
                        _pool.addFirst(new ObjectTimestampPair(obj));
                    } else {
                        _pool.addLast(new ObjectTimestampPair(obj));
                    }
                    if (decrementNumActive) {
                        _numActive--;
                    }
                    allocate();
                }
            }
        }
        ...
    }

把连接对象添加回池子或者销毁后,numActive计数会减1。

3 结论

数据库访问超时,连接已经关闭,释放的时候,直接抛出异常,没有对活跃连接数进行减1操作。最终导致活跃连接数达到上限并且无法再减少,相当于数据库连接池耗尽,新来的请求无法拿到连接而永久hang住。

解决方案

问题发生时,使用是commons-dbcp-1.2.1.jar,升级成commons-dbcp-1.4.jar即可解决该问题。看一下commons-dbcp-1.4.jar中PoolableConnection.close的逻辑:

     public synchronized void close() throws SQLException {
        ...
        if (!isUnderlyingConectionClosed) {
            // Normal close: underlying connection is still open, so we
            // simply need to return this proxy to the pool
            try {
                _pool.returnObject(this); // XXX should be guarded to happen at most once
            } catch(IllegalStateException e) {
                // pool is closed, so close the connection
                passivate();
                getInnermostDelegate().close();
            } catch(SQLException e) {
                throw e;
            } catch(RuntimeException e) {
                throw e;
            } catch(Exception e) {
                throw (SQLException) new SQLException("Cannot close connection (return to pool failed)").initCause(e);
            }
        } else {
            // Abnormal close: underlying connection closed unexpectedly, so we
            // must destroy this proxy
            try {
                _pool.invalidateObject(this); // XXX should be guarded to happen at most once
            } catch(IllegalStateException e) {
                // pool is closed, so close the connection
                passivate();
                getInnermostDelegate().close();
            } catch (Exception ie) {
                // DO NOTHING, "Already closed" exception thrown below
            }
            throw new SQLException("Already closed.");
        }
    }

可以看到,当底层连接已经关闭时,会调用GenericObjectPool.invalidateObject方法销毁连接对象:

    public void invalidateObject(Object obj) throws Exception {
        try {
            if (_factory != null) {
                _factory.destroyObject(obj);
            }
        } finally {
            synchronized (this) {
                _numActive--;
                allocate();
            }
        }
    }

在销毁逻辑中,对numActive计数进行了减1。

所以commons-dbcp-1.4.jar这个版本的包修复了commons-dbcp-1.2.1.jar包中连接池耗尽的问题。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用DBCP连接需要在配置文件中设置以下属性: 1. 驱动类名(driverClassName):数据库驱动程序的名称,例如com.mysql.jdbc.Driver。 2. 数据库连接URL(url):连接到数据库的URL,例如jdbc:mysql://localhost:3306/test。 3. 数据库用户名(username):连接到数据库所需的用户名。 4. 数据库密码(password):连接到数据库所需的密码。 5. 初始连接数(initialSize):连接在启动时创建的初始连接数。 6. 最小空闲连接数(minIdle):连接中保留的最小空闲连接数。 7. 最大活动连接数(maxActive):连接中同时可分配的最大活动连接数。 8. 最大等待时间(maxWait):等待连接分配连接的最长时间(以毫秒为单位)。 9. 连接名(poolName):连接的名称。 下面是一个示例配置文件: ```xml <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/test" /> <property name="username" value="root" /> <property name="password" value="password" /> <property name="initialSize" value="5" /> <property name="minIdle" value="2" /> <property name="maxActive" value="20" /> <property name="maxWait" value="10000" /> <property name="poolPreparedStatements" value="true" /> <property name="maxOpenPreparedStatements" value="100" /> <property name="poolName" value="MyDBCP" /> </bean> ``` 在上面的配置中,我们使用了org.apache.commons.dbcp.BasicDataSource类,它是DBCP连接的实现类。我们设置了MySQL数据库的驱动程序名称、URL、用户名和密码。我们还设置了连接的一些属性,例如初始连接数、最小空闲连接数、最大活动连接数和最大等待时间。最后,我们指定了连接的名称为MyDBCP
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值