Resolving Long Transactions and Connection‑Closed Issues in Spring with Druid
This article analyzes why a Spring service encounters "connection closed" errors during long‑running transactions, explains how MyBatis and Druid manage connections, and provides a programmatic‑transaction solution that splits large operations into smaller units to prevent connection leaks and timeouts.
The author received an alert that a database connection had been closed and discovered two main problems: exceptions caught in a try block were not re‑thrown, and a long‑running transaction kept the connection open beyond Druid's removeAbandonedTimeout , causing the pool to reclaim it.
Exception handling : The original code only performed a rollback and logged the error. The fix is to unwrap the throwable and re‑throw it, either via MyBatis' MyBatisExceptionTranslator or a custom exception, ensuring the service layer knows the failure.
// Case 1: MyBatisExceptionTranslator example
batchSqlSession.rollback();
Throwable unwrapped = ExceptionUtil.unwrapThrowable(e);
if (unwrapped instanceof RuntimeException) {
MyBatisExceptionTranslator translator = new MyBatisExceptionTranslator(
sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true);
throw Objects.requireNonNull(translator.translateExceptionIfPossible((RuntimeException) unwrapped));
}
throw new CommonException(unwrapped);
// Case 2: Simple custom exception
batchSqlSession.rollback();
throw new CustomException(e);Long transaction causing connection closure : The service method is annotated with @Transactional and performs file parsing, batch inserts, and comparisons—all within a single transaction. Because the file parsing can take >60 seconds, Druid's removeAbandoned (enabled) reclaims the connection, leading to the error.
The author inspected the Druid configuration:
spring:
datasource:
druid:
remove-abandoned: true
remove-abandoned-timeout: 60
log-abandoned: trueThese settings detect and log connections that remain idle beyond the timeout, helping to identify leaks.
Investigation showed that Spring obtains the JDBC connection at the very start of the transaction (in DataSourceTransactionManager#doBegin ), so the long‑running file parsing holds the connection for the whole method execution.
Druid's DestroyTask runs every timeBetweenEvictionRunsMillis (default 1 minute) and removes connections whose idle time exceeds removeAbandonedTimeoutMillis . Enabling logAbandoned prints the owning thread and stack trace.
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);
}
}
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");
}Solution : The author moved the time‑consuming file parsing out of the transactional scope and switched to programmatic transactions using TransactionTemplate . This splits the work into two smaller transactions, preventing the connection from being held for too long.
@Autowired
private TransactionTemplate transactionTemplate;
@Transactional(rollbackFor = Exception.class)
@Override
public Integer billCheck() {
Strategy strategy = getStrategy();
if (!requiredParamsExist()) {
return false;
}
try {
List
fileData = strategy.parseFile(file);
int affected = transactionTemplate.execute(status -> strategy.handleFileData(fileData));
if (affected > 0) {
List
compareResult = strategy.doBillCheck(params);
transactionTemplate.execute(status -> {
batchUtils.batchUpdateOrInsert(successData, Mapper.class, (e, m) -> m.insert(e));
batchUtils.batchUpdateOrInsert(failureData, Mapper.class, (e, m) -> m.insert(e));
batchUtils.batchUpdateOrInsert(updateData, Mapper.class, (e, m) -> m.update(e));
return true;
});
}
strategy.sendRobotMessage();
log.info("耗时:{}毫秒", elapsed);
} catch (Exception e) {
log.error("对账出错", e);
throw new CommonException("对账出错");
}
return affected;
}The article concludes that declarative transactions have a granularity limited to the method level; therefore, developers should avoid placing unrelated operations (e.g., RPC calls, file parsing) inside a transaction and should split large transactions into smaller, focused units.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.