Backend Development 9 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Transaction Deep Dive: Thread‑Bound Connections and Multithreaded Consistency

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.

JavaSpringmultithreadingthreadlocalTransaction ManagementJdbcTemplate
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.