Spring Transaction Deep Dive: Thread‑Bound Connections and Multithreaded Consistency
This article explains Spring's transaction core, showing how TransactionInterceptor and TransactionAspectSupport use AOP to bind a Connection to ThreadLocal, walks through key code snippets for creating, committing, and rolling back transactions, and demonstrates a JUC‑based solution for maintaining transaction consistency across multiple threads.
1. Transaction Principles
Spring manages transactions through AOP, binding the obtained Connection object to the current thread context via ThreadLocal . The central interceptor is TransactionInterceptor , which delegates to its superclass TransactionAspectSupport .
<code>public class TransactionInterceptor {
public Object invoke(MethodInvocation invocation) {
// Core method call
return invokeWithinTransaction(...);
}
}
</code>The parent class provides the main execution flow:
<code>public abstract class TransactionAspectSupport {
protected Object invokeWithinTransaction(...) {
// 1.1 Create transaction object
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
try {
// Invoke next interceptor or target method
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// 1.2 Rollback transaction
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
// Reset ThreadLocal TransactionInfo
cleanupTransactionInfo(txInfo);
}
// 1.3 Commit or rollback transaction
commitTransactionAfterReturning(txInfo);
return retVal;
}
}
</code>Creating the transaction object involves TransactionInfo and TransactionStatus :
<code>protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr,
final String joinpointIdentification) {
TransactionStatus status = null;
if (txAttr != null && tm != null) {
// Create transaction status object
status = tm.getTransaction(txAttr);
}
// Wrap status into TransactionInfo and bind to current thread
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}
</code>The concrete AbstractPlatformTransactionManager creates the transaction status and starts the transaction:
<code>public abstract class AbstractPlatformTransactionManager {
public final TransactionStatus getTransaction(...) {
if (isExistingTransaction(transaction)) {
return handleExistingTransaction(def, transaction, debugEnabled);
}
if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
}
if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
throw new IllegalTransactionStateException("No existing transaction found for transaction marked with propagation 'mandatory'");
}
if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
return startTransaction(def, transaction, debugEnabled, suspendedResources);
}
// other propagation behaviors omitted for brevity
}
}
</code>When a new transaction starts, DataSourceTransactionManager obtains a Connection and binds it to the thread:
<code>public class DataSourceTransactionManager {
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Connection newCon = obtainDataSource().getConnection();
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
} catch (SQLException ex) {
// handle exception
}
}
}
</code>When using JdbcTemplate , the connection is retrieved from the thread‑bound resource via DataSourceUtils :
<code>public class JdbcTemplate {
private <T> T execute(...) {
// Obtain connection from ThreadLocal
Connection con = DataSourceUtils.getConnection(obtainDataSource());
// ... execute SQL ...
}
}
</code> <code>public abstract class DataSourceUtils {
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {
return doGetConnection(dataSource);
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
}
}
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
// Retrieve connection holder from ThreadLocal
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
if (!conHolder.hasConnection()) {
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
// fallback to obtaining a new connection
return dataSource.getConnection();
}
}
</code>Thus, each thread holds its own Connection , ensuring isolation.
2. Multithreaded Transaction Consistency
To keep transaction consistency across multiple threads, JUC utilities such as CountDownLatch , AtomicBoolean , and CompletableFuture are employed. The following example demonstrates a service that inserts two records in parallel while sharing the same transaction context.
<code>static class PersonService {
@Resource
private JdbcTemplate jdbcTemplate;
@Resource
private DataSource dataSource;
@Transactional
public void save() throws Exception {
CountDownLatch cdl = new CountDownLatch(2);
AtomicBoolean txRollback = new AtomicBoolean(false);
// Thread 1
CompletableFuture.runAsync(() -> {
Person p1 = new Person();
p1.setAge(1);
p1.setName("张三");
transactionTemplate.execute(status -> {
int result = 0;
try {
result = jdbcTemplate.update("insert into t_person (age, name) values (?, ?)", p1.getAge(), p1.getName());
} catch (Exception e) {
txRollback.set(true);
}
try {
cdl.countDown();
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (txRollback.get()) {
status.setRollbackOnly();
}
System.out.printf("%s Insert Operator Result: %d 次%n", Thread.currentThread().getName(), result);
return result;
});
});
// Thread 2
transactionTemplate.execute(status -> {
Person p2 = new Person();
p2.setAge(2);
p2.setName("李四");
int result = 0;
try {
result = jdbcTemplate.update("insert into t_person (age, name) values (?, ?)", p2.getAge(), p2.getName());
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
txRollback.set(true);
}
try {
cdl.countDown();
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (txRollback.get()) {
status.setRollbackOnly();
}
System.out.printf("%s Insert Operator Result: %d 次%n", Thread.currentThread().getName(), result);
return result;
});
cdl.await();
System.err.println("Operator Complete...");
}
}
</code>The latch synchronizes the two threads, while the atomic flag records any exception. After both threads finish, the flag determines whether the surrounding transaction should be rolled back, ensuring consistency even in a multithreaded environment.
Understanding the underlying transaction mechanism makes it possible to devise simpler solutions, which will be explored in future articles.
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.