背景
公司线上运行的项目最近报了这个错,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事务管理自动关闭数据源连接池的功能,了解了原理才好解决问题。