vlambda博客
学习文章列表

数据源连接池未关闭的问题 Could not open JDBC Connection for transaction

背景

公司线上运行的项目最近报了这个错,Could not open JDBC Connection for transaction,无法获取数据源连接池了。

分析

阅读源码,看看各个情况下是否都能自动释放数据源连接吧。

MyBatis释放连接

MyBatis自己单独运行的时候运行SQL语句是不会自动释放数据源连接的,但和Spring整合后就会自动释放数据源连接了。Spring改变了MyBatis的SqlSession,改成Spring整合包中的SqlSessionTemplate,关键代码如下:

public class SqlSessionTemplate implements SqlSessionDisposableBean {
    //...
    //省略一些代码
    private class SqlSessionInterceptor implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            SqlSession sqlSession = getSqlSession(
                    SqlSessionTemplate.this.sqlSessionFactory,
                    SqlSessionTemplate.this.executorType,
                    SqlSessionTemplate.this.exceptionTranslator);
            try {
                Object result = method.invoke(sqlSession, args);
                if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                    sqlSession.commit(true);
                }
                return result;
            } catch (Throwable t) {
                Throwable unwrapped = unwrapThrowable(t);
                if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                    // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
                    closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                    sqlSession = null;
                    Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
                    if (translated != null) {
                        unwrapped = translated;
                    }
                }
                throw unwrapped;
            } finally {
                if (sqlSession != null) {
                    closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                }
            }
        }
    }
}

在最后的finally中,会关闭session,释放数据源连接。

事务@Transactional释放连接

在方法上添加注解@Transactional将该方法标记成事务,也会自动释放连接,关键代码如下:

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
        implements ResourceTransactionManagerInitializingBean 
{
    //...
    //省略一些代码
    @Override
    protected void doCleanupAfterCompletion(Object transaction) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        if (txObject.isNewConnectionHolder()) {
            TransactionSynchronizationManager.unbindResource(this.dataSource);
        }
        Connection con = txObject.getConnectionHolder().getConnection();
        try {
            if (txObject.isMustRestoreAutoCommit()) {
                con.setAutoCommit(true);
            }
            DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
        }
        catch (Throwable ex) {
            logger.debug("Could not reset JDBC Connection after transaction", ex);
        }
        if (txObject.isNewConnectionHolder()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Releasing JDBC Connection [" + con + "] after transaction");
            }
            DataSourceUtils.releaseConnection(con, this.dataSource);
        }
        txObject.getConnectionHolder().clear();
    }

这其中,DataSourceUtils.releaseConnection(con, this.dataSource)方法会关闭数据源连接。

找问题

公司项目用的是Druid数据源,最大连接数设的50,按照上面的分析,一般情况下是不可能用完的,肯定是有代码没有释放连接。
找了好半天,最终定位到如下代码:

@Autowired
private SqlSessionFactory sqlSessionFactory;
public void batchInsert(List<TaskCenter> list) {
    if(list == null || list.size() == 0){
        return;
    }
    try {
        SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        TaskCenterMapper mapper = sqlSession.getMapper(TaskCenterMapper.class);
        for(TaskCenter taskCenter : list){
            mapper.insertSelective(taskCenter);
        }
        sqlSession.flushStatements();
        sqlSession.commit();
        log.info("批量插入成功: " + list.size()+"条数据");
    }catch (Exception ex){
        log.error("批量插入失败: ", ex);
    }
}

这段代码的意思是使用MyBatis的批量插入功能批量插入数据,我们上面分析过,使用MyBatis的SqlSession是不会自动关闭数据源连接的,需要使用Spring包装过的SqlSessionTemplate才会自动关闭数据源连接。所以每次执行这个batchInsert方法,都会占用一个数据源连接而不会释放,最终导致数据源连接池被占满,无法开启新的连接。

解决问题

根据以上的分析,现在有两种方案可以解决该问题
1、将该方法加入事务,在方法上增加注解@Transactional,代码如下:

@Transactional
public void batchInsert(List<TaskCenter> list) {
    if(list == null || list.size() == 0){
        return;
    }
    // 以下省略
    // ...

2、使用完sqlSession后手动关闭sqlSession,代码如下:

public void batchInsert(List<TaskCenter> list) {
    if(list == null || list.size() == 0){
        return;
    }
    SqlSession sqlSession = null;
    try {
        sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        TaskCenterMapper mapper = sqlSession.getMapper(TaskCenterMapper.class);
        for(TaskCenter taskCenter : list){
            mapper.insertSelective(taskCenter);
        }
        sqlSession.flushStatements();
        sqlSession.commit();
        log.info("批量插入成功: " + list.size()+"条数据");
    }catch (Exception ex){
        log.error("批量插入失败: ", ex);
    }finally {
        if (sqlSession != null) {
            SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
        }
    }
}

结语

这篇根据一个生产上的问题通过分析源码了解了MyBatis框架和Spring事务管理自动关闭数据源连接池的功能,了解了原理才好解决问题。