Mastering Spring Transaction Timeouts: Configurations, Tests, and Internals
This article explains how to configure transaction timeout in Spring 5.3.23 using annotations and programmatic approaches, demonstrates four test scenarios with a large table, analyzes the resulting timeout exceptions, and delves into the underlying mechanisms in DataSourceTransactionManager, JdbcTemplate, and related utility classes.
Environment: Spring 5.3.23
1. Configure Transaction Timeout
Annotation method:
<code>// unit is seconds
@Transactional(timeout = 2)
public void save() {
}
</code>Programmatic method 1:
<code>@Resource
private PlatformTransactionManager tm;
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setTimeout(2);
</code>Programmatic method 2:
<code>@Resource
private PlatformTransactionManager tm;
public void update() {
TransactionTemplate template = new TransactionTemplate(tm);
template.setTimeout(2);
template.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus status) {
// ...
return null;
}
});
}
</code>2. Prepare Environment
Create a table with 20 million rows and no indexes except the primary key.
3. Simulate Transaction Timeout
Test 1
Count rows inside a transaction with a 20‑second timeout.
<code>// Set timeout to 20s
@Transactional(timeout = 20)
public void query() {
long start = System.currentTimeMillis();
jdbcTemplate.execute("select count(*) from p_user");
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "毫秒");
}
</code>Result: 3198 ms (within the timeout).
Changing the timeout to 3 seconds triggers a MySQLTimeoutException from the driver:
<code>com.mysql.cj.jdbc.exceptions.MySQLTimeoutException: Statement cancelled due to timeout or client request
at com.mysql.cj.jdbc.StatementImpl.checkCancelTimeout(StatementImpl.java:2167)
</code>Test 2
Sleep 3 seconds before the DB operation, timeout set to 2 seconds.
<code>@Transactional(timeout = 2)
public void query() {
long start = System.currentTimeMillis();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
jdbcTemplate.execute("select 1");
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "毫秒");
}
</code>Result: Spring throws a TransactionTimedOutException.
Test 3
Execute the DB operation first, then sleep 3 seconds (still timeout 2 seconds).
<code>@Transactional(timeout = 2)
public void query() {
long start = System.currentTimeMillis();
jdbcTemplate.execute("select 1");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "毫秒");
}
</code>Result: The method finishes normally (≈3015 ms) because the timeout is checked only before each DB call.
Test 4
Same as Test 3 but perform a second DB call after the sleep.
<code>@Transactional(timeout = 2)
public void query() {
long start = System.currentTimeMillis();
jdbcTemplate.execute("select 1");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "毫秒");
// second DB operation
jdbcTemplate.execute("select 1");
}
</code>Result: The first DB call succeeds; the second triggers a TransactionTimedOutException, confirming that the timeout is evaluated at each database interaction.
4. Transaction Timeout Mechanism
When a transaction starts, DataSourceTransactionManager#doBegin sets the timeout:
<code>protected void doBegin(Object transaction, TransactionDefinition definition) {
int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}
}
protected int determineTimeout(TransactionDefinition definition) {
if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) {
return definition.getTimeout();
}
return getDefaultTimeout();
}
</code>When JdbcTemplate creates a Statement , it applies the timeout:
<code>protected void applyStatementSettings(Statement stmt) throws SQLException {
DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout());
}
</code>DataSourceUtils.applyTimeout chooses the remaining transaction timeout or the explicit value:
<code>public static void applyTimeout(Statement stmt, @Nullable DataSource dataSource, int timeout) throws SQLException {
ConnectionHolder holder = null;
if (dataSource != null) {
holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
}
if (holder != null && holder.hasTimeout()) {
stmt.setQueryTimeout(holder.getTimeToLiveInSeconds());
} else if (timeout >= 0) {
stmt.setQueryTimeout(timeout);
}
}
</code>ResourceHolderSupport computes the remaining time and throws TransactionTimedOutException when the deadline is reached:
<code>public int getTimeToLiveInSeconds() {
double diff = ((double) getTimeToLiveInMillis()) / 1000;
int secs = (int) Math.ceil(diff);
checkTransactionTimeout(secs <= 0);
return secs;
}
private void checkTransactionTimeout(boolean deadlineReached) throws TransactionTimedOutException {
if (deadlineReached) {
setRollbackOnly();
throw new TransactionTimedOutException("Transaction timed out: deadline was " + this.deadline);
}
}
</code>In summary, the transaction start records the deadline; each database operation checks the elapsed time against this deadline, and if exceeded, Spring rolls back the transaction and throws a timeout exception.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.