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 2 main types of transaction management in Spring. They are declarative transaction management which is a high level one and programmatic transaction management which is more advance 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 rises 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 run time errors or unexpected results if you try to do so.
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 rises 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 run time 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 static 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 form another thread, a run time 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 life cycle 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 through out the transaction.
How to use spring transactions with multiple threads?
Now you can 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 we have 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 database processing element?
The database connector and the database processing element provide the data persistent capabilities to the project-x. There are three main components to perform database operations such as db egress connector, db ingress connector and the db processing element. Database processing element supports for all 4 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 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. Second table is named as students and the columns are id, school and the 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 take a clone of the message and send 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] do 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 return 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. Last database processing 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.
Comments
Post a Comment