Ensuring Transaction Consistency in Multithreaded Spring Applications Using Programmatic Transactions
This article explains how to execute two dependent tasks in parallel, guarantee their successful completion before a final step, and maintain transaction consistency across multiple threads in Spring by using CompletableFuture, programmatic transaction management, and a custom transaction‑resource copying mechanism.
Problem Statement
The original requirement is to delete authority modules and their sub‑modules in parallel, then delete the parent module and finally commit the whole transaction, while ensuring that the parallel steps succeed before the final step.
Asynchronous Execution
Using @Async is insufficient for this scenario, so CompletableFuture is introduced to run the two delete operations concurrently and wait for both to finish before invoking removeById:
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);
}Transaction Consistency in Multithreaded Environment
Declarative @Transactional cannot guarantee consistency across threads because Spring binds transaction resources to the thread‑local map of the thread that creates the transaction. When the commit is attempted from a different thread, the required resources are missing, leading to errors such as "No value for key [HikariDataSource] bound to thread".
Programmatic Transaction Review
Spring’s transaction infrastructure consists of TransactionDefinition, PlatformTransactionManager, and TransactionStatus. A typical programmatic transaction looks like:
TransactionStatus ts = transactionManager.getTransaction(definition);
try {
// business logic
transactionManager.commit(ts);
} catch (Exception e) {
transactionManager.rollback(ts);
}Solution: Copy Transaction Resources Between Threads
A custom TransactionResource class captures the thread‑local transaction state (resources map, synchronizations, transaction name, read‑only flag, isolation level, active flag) and can re‑bind it in another thread before committing or rolling back.
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);
}The manager runs each task in a separate thread, opens a new transaction, copies the transaction context, executes the task, and finally either commits all transactions or rolls them back if any task fails.
Result
With the resource‑copying approach, all threads share the same transactional context, ensuring that either all deletions are committed or all are rolled back, as demonstrated by the test cases where exceptions trigger a full rollback.
Conclusion
While declarative transactions are convenient, understanding Spring’s underlying transaction mechanics is essential for advanced scenarios such as multithreaded consistency; programmatic transactions combined with explicit resource propagation provide a reliable solution.
IT Xianyu
We share common IT technologies (Java, Web, SQL, etc.) and practical applications of emerging software development techniques. New articles are posted daily. Follow IT Xianyu to stay ahead in tech. The IT Xianyu series is being regularly updated.
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.
