Backend Development 19 min read

Ensuring Transaction Consistency in Multi‑threaded Spring Applications Using Programmatic Transactions

This article explains how to execute two dependent tasks in parallel with CompletableFuture, why the usual @Transactional annotation fails in a multi‑threaded environment, and provides a programmatic transaction solution that copies Spring's transaction resources between threads to guarantee atomic commit or rollback across all parallel operations.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Ensuring Transaction Consistency in Multi‑threaded Spring Applications Using Programmatic Transactions

Problem Statement

The original method removeAuthorityModuleSeq deletes resources in three sequential steps, but the requirement is to run steps 1 and 2 in parallel, ensure both succeed, then execute step 3 and finally commit the whole transaction.

Why @Async Is Not Sufficient

Spring's @Async creates a proxy that wraps the method in a MethodInterceptor (specifically AsyncExecutionInterceptor ) and runs it in a thread pool, but it does not provide the binary dependency control needed for the described scenario.

Using CompletableFuture for Parallel Execution

public void removeAuthorityModuleSeq(Integer authorityModuleId,
                                   IAuthorityService iAuthorityService,
                                   IRoleAuthorityService iRoleAuthorityService) {
    CompletableFuture.runAsync(() -> {
        // two parallel tasks
        CompletableFuture
future1 = CompletableFuture.runAsync(() ->
            deleteAuthoritiesOfCurrentAuthorityModule(authorityModuleId, iAuthorityService, iRoleAuthorityService), executor);
        CompletableFuture
future2 = CompletableFuture.runAsync(() ->
            deleteSonAuthorityModuleUnderCurrentAuthorityModule(authorityModuleId, iAuthorityService, iRoleAuthorityService), executor);
        // wait for both then run step 3
        CompletableFuture.allOf(future1, future2).thenRun(() -> removeById(authorityModuleId));
    }, executor);
}

Transaction Consistency Challenge in Multi‑threaded Context

When the tasks run in separate threads, Spring's thread‑local transaction resources (managed by TransactionSynchronizationManager ) are bound to the worker threads, not the main thread that later attempts to commit, leading to errors such as "No value for key [HikariDataSource (HikariPool-1)] bound to thread [main]".

Review of Spring Transaction Mechanics

Spring transaction handling involves three phases—creation, execution, and completion—using TransactionDefinition , PlatformTransactionManager , and TransactionStatus . Declarative transactions rely on AOP proxies that insert a TransactionInterceptor into the call chain.

Programmatic Transaction Approach

Because declarative @Transactional cannot span multiple threads, a programmatic transaction is required. The following example shows a simple programmatic transaction that opens a new transaction, performs business logic, and commits or rolls back based on success.

public class TransactionMain {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        test();
    }
    private static void test() {
        DataSource dataSource = getDS();
        JdbcTransactionManager jtm = new JdbcTransactionManager(dataSource);
        DefaultTransactionDefinition transactionDef = new DefaultTransactionDefinition();
        TransactionStatus ts = jtm.getTransaction(transactionDef);
        try {
            update(dataSource);
            jtm.commit(ts);
        } catch (Exception e) {
            jtm.rollback(ts);
            System.out.println("发生异常,我已回滚");
        }
    }
    private static void update(DataSource dataSource) throws Exception {
        JdbcTemplate jt = new JdbcTemplate();
        jt.setDataSource(dataSource);
        jt.update("UPDATE Department SET Dname=\"大忽悠\" WHERE id=6");
        throw new Exception("我是来捣乱的");
    }
}

Copying Transaction Resources Between Threads

The core solution is to capture the current thread's transaction resources, copy them, and bind them to each worker thread before executing the task. The TransactionResource class encapsulates the resource map, synchronizations, and transaction metadata.

public class MultiplyThreadTransactionManager {
    private final DataSource dataSource;
    public void runAsyncButWaitUntilAllDown(List
tasks, Executor executor) {
        if (executor == null) throw new IllegalArgumentException("线程池不能为空");
        DataSourceTransactionManager transactionManager = getTransactionManager();
        AtomicBoolean ex = new AtomicBoolean();
        List
> taskFutureList = new ArrayList<>(tasks.size());
        List
transactionStatusList = new ArrayList<>(tasks.size());
        List
transactionResources = new ArrayList<>(tasks.size());
        tasks.forEach(task -> {
            taskFutureList.add(CompletableFuture.runAsync(() -> {
                try {
                    transactionStatusList.add(openNewTransaction(transactionManager));
                    transactionResources.add(TransactionResource.copyTransactionResource());
                    task.run();
                } catch (Throwable t) {
                    t.printStackTrace();
                    ex.set(true);
                    taskFutureList.forEach(f -> f.cancel(true));
                }
            }, executor));
        });
        try {
            CompletableFuture.allOf(taskFutureList.toArray(new CompletableFuture[0])).get();
        } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }
        if (ex.get()) {
            System.out.println("发生异常,全部事务回滚");
            for (int i = 0; i < tasks.size(); i++) {
                transactionResources.get(i).autoWiredTransactionResource();
                transactionManager.rollback(transactionStatusList.get(i));
                transactionResources.get(i).removeTransactionResource();
            }
        } else {
            System.out.println("全部事务正常提交");
            for (int i = 0; i < tasks.size(); i++) {
                transactionResources.get(i).autoWiredTransactionResource();
                transactionManager.commit(transactionStatusList.get(i));
                transactionResources.get(i).removeTransactionResource();
            }
        }
    }
    private TransactionStatus openNewTransaction(DataSourceTransactionManager tm) {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        return tm.getTransaction(def);
    }
    private DataSourceTransactionManager getTransactionManager() {
        return new DataSourceTransactionManager(dataSource);
    }
    @Builder
    private static class TransactionResource {
        private Map
resources = new HashMap<>();
        private Set
synchronizations = new HashSet<>();
        private String currentTransactionName;
        private Boolean currentTransactionReadOnly;
        private Integer currentTransactionIsolationLevel;
        private Boolean actualTransactionActive;
        public static TransactionResource copyTransactionResource() {
            return TransactionResource.builder()
                .resources(TransactionSynchronizationManager.getResourceMap())
                .synchronizations(new LinkedHashSet<>())
                .currentTransactionName(TransactionSynchronizationManager.getCurrentTransactionName())
                .currentTransactionReadOnly(TransactionSynchronizationManager.isCurrentTransactionReadOnly())
                .currentTransactionIsolationLevel(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel())
                .actualTransactionActive(TransactionSynchronizationManager.isActualTransactionActive())
                .build();
        }
        public void autoWiredTransactionResource() {
            resources.forEach(TransactionSynchronizationManager::bindResource);
            TransactionSynchronizationManager.initSynchronization();
            TransactionSynchronizationManager.setActualTransactionActive(actualTransactionActive);
            TransactionSynchronizationManager.setCurrentTransactionName(currentTransactionName);
            TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(currentTransactionIsolationLevel);
            TransactionSynchronizationManager.setCurrentTransactionReadOnly(currentTransactionReadOnly);
        }
        public void removeTransactionResource() {
            resources.keySet().forEach(key -> {
                if (!(key instanceof DataSource)) {
                    TransactionSynchronizationManager.unbindResource(key);
                }
            });
        }
    }
}

Testing the Solution

A test case creates two Runnable tasks—one that deletes a user and deliberately throws an exception, and another that deletes a sign record. When executed through runAsyncButWaitUntilAllDown , both transactions are rolled back, leaving the database unchanged.

Conclusion

The article demonstrates that declarative @Transactional is insufficient for multi‑threaded scenarios and that copying Spring's transaction resources between threads enables reliable programmatic transaction management. Alternative approaches include direct JDBC transaction control or distributed transaction frameworks.

Backend DevelopmentspringCompletableFuturemultithreadingtransaction managementProgrammatic Transaction
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.