Spring Transaction Management Over Multiple Threads
Learn why Spring transactions over multiple threads fail, and how to use them successfully, plus practical applications of multi-threaded database transactions.
Join the DZone community and get the full member experience.
Join For FreeThe Spring framework provides a comprehensive API for database transaction management. Spring takes care of all underlying transaction management considerations and provides a consistent programming model for different transaction APIs such as Java Transaction API (JTA), JDBC, Hibernate, Java Persistence API (JPA), and Java Data Objects (JDO). There are two main types of transaction management in Spring: declarative transaction management, which is a high level one, and programmatic transaction management, which is more advanced but flexible.
The Spring API works very well with almost all of the transaction management requirements as long as the transaction is on a single thread. The problem arises when we want to manage a transaction across multiple threads. Spring doesn't support transactions over multiple threads out of the box. Spring doesn't explicitly mention that in the documentation, but you will end up with runtime errors or unexpected results if you try to do so.
Why Do Spring Transactions Over Multiple Threads Fail?
Spring stores a set of thread locals inside the org.springframework.transaction.support.TransactionSynchronizationManager
class. These thread locals are specific for an ongoing transaction on a single thread (Thread locals values are specific for a single thread. Thread local value set by one thread cannot be accessed by another thread).
public abstract class TransactionSynchronizationManager {
private stati final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
}
If we start the transaction from one thread and try to commit or rollback the transaction from another thread, a runtime error will be generated complaining that the Spring transaction is not active on the current thread. Though we start and end the transaction from the same thread, we cannot perform database operations belong to transaction from another thread too.
When we initialize the transaction the actualTransactionActive
thread local is set to true. synchronizations
thread local is also initialized. Other thread locals are also accessed and updated during the lifecycle of the transaction. When we try to commit or rollback the transaction at the end of the transaction scope, values of these thread locals are again checked. What happens if we use multiple threads for the transaction is that these thread local values are not visible across multiple threads. Therefore, Spring cannot maintain the transaction state throughout the transaction.
How to Use Spring Transactions With Multiple Threads
Now you understand the problem with spring transactions over multiple threads. The thread local values are not propagating to new threads from old threads. The only solution here is to manually copy these thread local values to newly spawned threads to keep the transaction unbroken.
As mentioned above, actualTransactionActive
thread local is used to check whether the transaction is active on the current thread. This thread local is set true on the thread which initializes the transaction. But, since this thread local value is not visible to other threads, we have to maintain another boolean flag to inform the activeness of the transaction to other threads. Then we can check that flag from the new thread and set actualTransactionActive
thread local to true manually. Thread local value can be set by calling following code line:
TransactionSynchronizationManager.setActualTransactionActive(true);
Practical Applications of Multi-Threaded Database Transactions
Database processing element of Adroitlogic Project-X is a good industrial level practical application of multi-threaded database transactions.
What Is Project-X?
Project X is the base framework for a new generation of redesigned integration products by Adroitlogic. Project-X consists of all the API definitions, core implementations of those APIs, messaging engine, message format definitions and implementations, metrics engine, etc. to be used by those integration products.
What Is a Database Processing Element?
The database connector and the database processing element provide the data persistent capabilities to Project-x. There are three main components to perform database operations: db egress connector, db ingress connector, and the db processing element. Database processing element supports for all four CRUD operations. Database egress connector provides create, update, delete operations and database ingress connector provides read operation in a timed manner.
How Transactions Are Used in The Database Processor
Let’s see how to configure an integration flow to perform a database transactional operation. Here we enter the data to a table, obtain the id of the last inserted row, and then again insert data to another table with the id obtained from the previous database operation.
Suppose there is a database table named as people having columns named as id, name, and age. The second table is named as students and the columns are id, school, and grade. Here, the id of the students table is a foreign key referenced from the people table.
The database transaction scope start element[1] starts the transaction scope. All the database operations happening within transaction scope are guaranteed to be committed if and only if all the operations within the transaction scope were a success. Then the clone message processing element[2] just takes a clone of the message and sends one copy as the response to the nio http ingress connector and the other copy to the database processor. Two new threads are created here and thread locals of the TransactionSynchronizationManager
class are copied to the newly created threads. Then, database processing element[3] does the first insert to the people table in the database. Next, database processing element[5] reads the last inserted row from the people table and returns the result in DBResultsSetFormat
. Then we use a custom processing element[6] to extract the id from the DBResultsSetFormat
and set it to the header of the message. The last database processing element, element[7], does the second insert to the students table using the data read from the payload the id read from the message header. Finally, the database transaction scope end element closes the transaction scope and commit the changes to the database if all the operations are complete. The database transaction will be rolled back in any kind of failure within the transaction scope of the flow.
Published at DZone with permission of Dulaj Atapattu. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments