背景

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

分析

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

MyBatis释放连接

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

public class SqlSessionTemplate implements SqlSession, DisposableBean {
   
    //...
    //省略一些代码
    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 ResourceTransactionManager, InitializingBean {
   
	//...
    //省略一些代码
	@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事务管理自动关闭数据源连接池的功能,了解了原理才好解决问题。