Ensuring Transaction Consistency in Multi‑threaded Spring Applications Using Programmatic Transactions
The article explains how to execute parallel tasks with CompletableFuture, why @Async cannot guarantee transactional integrity in a multi‑threaded Spring environment, and presents a programmatic transaction approach—including copying transaction resources between threads—to achieve consistent commit or rollback across concurrent operations.
This article addresses a common business requirement: executing two cleanup steps in parallel, then performing a final delete step, and finally committing a single transaction in a Spring application.
Problem statement : The original method deletes authorities and sub‑modules sequentially. The goal is to run the two delete operations concurrently while ensuring that the final delete and transaction commit occur only after both succeed.
Why @Async is insufficient : Spring's @Async creates a proxy that submits the method to a thread pool, but it does not provide a mechanism for coordinating binary dependencies between tasks, nor does it maintain transaction context across threads.
Solution using CompletableFuture :
public void removeAuthorityModuleSeq(Integer authorityModuleId, IAuthorityService iAuthorityService, IRoleAuthorityService iRoleAuthorityService) {
CompletableFuture.runAsync(() -> {
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() ->
deleteAuthoritiesOfCurrentAuthorityModule(authorityModuleId, iAuthorityService, iRoleAuthorityService), executor);
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() ->
deleteSonAuthorityModuleUnderCurrentAuthorityModule(authorityModuleId, iAuthorityService, iRoleAuthorityService), executor);
CompletableFuture.allOf(future1, future2).thenRun(() -> removeById(authorityModuleId));
}, executor);
}While this achieves asynchronous execution, it still cannot guarantee transaction consistency because Spring binds transaction resources to the thread that created the transaction.
Spring transaction internals recap :
Transaction creation, execution, and completion are managed via TransactionDefinition, PlatformTransactionManager, and TransactionStatus.
Resources such as the JDBC Connection are stored in thread‑local maps managed by TransactionSynchronizationManager.
When a new thread attempts to commit or roll back, it cannot locate the resources bound to the original thread, leading to errors like
No value for key [HikariDataSource (HikariPool-1)] bound to thread [main].
Programmatic transaction approach :
public class TransactionMain {
public static void main(String[] args) throws Exception {
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("Exception occurred, rolled back");
}
}
private static void update(DataSource ds) throws Exception {
JdbcTemplate jt = new JdbcTemplate();
jt.setDataSource(ds);
jt.update("UPDATE Department SET Dname=\"大忽悠\" WHERE id=6");
throw new Exception("Intentional failure");
}
}To make this work across multiple threads, the article introduces a CopyTransactionResource utility that captures the thread‑local transaction state and re‑binds it in each worker thread before committing or rolling back.
public void runAsyncButWaitUntilAllDown(List<Runnable> tasks, Executor executor) {
if (executor == null) throw new IllegalArgumentException("Executor cannot be null");
DataSourceTransactionManager transactionManager = getTransactionManager();
AtomicBoolean ex = new AtomicBoolean();
List<CompletableFuture<?>> futures = new ArrayList<>();
List<TransactionStatus> statuses = new ArrayList<>();
List<TransactionResource> resources = new ArrayList<>();
for (Runnable task : tasks) {
futures.add(CompletableFuture.runAsync(() -> {
try {
statuses.add(openNewTransaction(transactionManager));
resources.add(TransactionResource.copyTransactionResource());
task.run();
} catch (Throwable t) {
t.printStackTrace();
ex.set(true);
futures.forEach(f -> f.cancel(true));
}
}, executor));
}
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get();
} catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }
if (ex.get()) {
System.out.println("Exception occurred, rolling back all");
for (int i = 0; i < tasks.size(); i++) {
resources.get(i).autoWiredTransactionResource();
transactionManager.rollback(statuses.get(i));
resources.get(i).removeTransactionResource();
}
} else {
System.out.println("All tasks succeeded, committing");
for (int i = 0; i < tasks.size(); i++) {
resources.get(i).autoWiredTransactionResource();
transactionManager.commit(statuses.get(i));
resources.get(i).removeTransactionResource();
}
}
}The accompanying TransactionResource class captures the maps and flags from TransactionSynchronizationManager and provides methods to bind and unbind them in the worker threads.
Testing : A JUnit test creates two tasks—one that deletes a user and deliberately throws an exception, and another that deletes a sign record. Using the manager, both tasks run concurrently; the exception triggers a rollback, leaving the database unchanged.
Conclusion : The article demonstrates that relying solely on declarative @Transactional is insufficient for multi‑threaded scenarios. By understanding Spring’s transaction infrastructure and applying programmatic transaction control with resource copying, developers can achieve reliable transaction consistency across parallel tasks.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
