Spring, Hibernate transactions. Joining a transaction in Thread B created in A. Possible?
Asked Answered
F

3

4

Is it possible to use a transaction in another thread?

Like pass a transaction created in thread A and then execute some logic in Thread B within the same transaction?

I have two queues and separate executors which handles population of certain Entity types.

However, a batch job is managing both population and waiting for each to finish. It would be unnecessary to create two transactions. If one fails, ideally I'd want all data to be rolled back so it would have been ideal to run them as one transaction as well as it provides improved performance.

So, is it possible to create one transaction, pass it along to another thread, execute some stuff within the boundaries of the first one?

I am using Spring and Hibernate and currently using

TransactionTemplate template = new TransactionTemplate( getTransactionManager() );
template.setPropagationBehavior(propagationBehavior);
template.setReadOnly(readOnly);

TransactionStatus status = getTransactionManager().getTransaction(template);

to create a transaction, not using annotations at all and have no plans to do so either.

Ferdinandferdinanda answered 6/5, 2015 at 14:20 Comment(0)
C
6

It's not possible with Spring. All the transaction based code eventually ends up in TransactionSynchronizationManager which is full of ThreadLocals and no way to copy those values from one thread to another.

If you want to be able to do this, you can use Spring to get the DataSource but you have to create your own connections and manually create Hibernate sessions. Spring's PlatformTransactionManager is out of the question.

[EDIT] What's the point of having several threads in your case? If you want to parallelize work, then the correct approach is to have N threads which prepare the data that should be inserted into the database and then a single thread which creates 1 transaction and then does all the work.

Yes, that means you need to keep a lot of data in memory.

If you really don't want to do that, then the next solution is to have work tables where each thread puts their results. When both threads finish, you start another thread which locks the work tables and runs a few SQL queries to copy the data to the correct place.

Always keep in mind that Database connections, SQL and threads don't mix. A database is global state. If you change global state from several places at the same time, you'll always face all kind of odd problems. Try to avoid that. Split the work into many small, independent tasks (i.e. which work perfectly fine when each of them has their own transaction). If you can't do that, you need to rethink your design until you can or you can't use threads.

Caddaric answered 6/5, 2015 at 14:26 Comment(7)
@Aoron Hmm.. I can't find any transaction methods on DataSource. What about using hibernate session.beginTransaction() ? org.hibernate.engine.transaction.internal.jdbc and jta .. would these possibly work?Ferdinandferdinanda
I am also considering using a main thread to handle the work across several threads and have them notify the work to be processed by the single thread, then wait for the signal from the main thread to continue. But not ideal.Ferdinandferdinanda
There aren't any method on DataSource. Spring wraps the DataSource and uses this wrapper in its implementation of PlatformTransactionManager. The implementation will use TransactionSynchronizationManager and the static ThreadLocals in there to make sure each thread gets a different connection or an error if something is off (which is what you'll see if you try your approach).Caddaric
@Aorn JTA transaction have a join method, however the implementation I am seeing is using an empty implementation.Ferdinandferdinanda
Regarding your edit. The problem with using several threads and several transaction is if several separate work processes can update a table row. One way to avoid that might be to use a worker thread and have the other work processes submit work to it. That way no more than one can ever concurrently update a table row and we avoid concurrent updates and difficult to resolve exceptions. It might also be possible to use a higher isolation level to avoid this, although spring in it's current form doesn't support setting of isolation levels other than on creation of the datasource.Ferdinandferdinanda
You can set the isolation level when you create a TransactionTemplate. Re Threads and TX: That's what I said. Threads only work when they are independent. Any common state causes trouble.Caddaric
yes, i am supposed to be able to do that, but isolation level can't be set on spring, at least not when I tried a couple of months ago. I've now implemented a thread lock that delegates work to another thread. I think that'll work in this scenario and a good class to have.Ferdinandferdinanda
D
4

In Spring, it is actually possible to transfer the transactional context from one thread to another with a simple trick as below:

  • All thread local variables related the transactional context on thread 1 must be retrieved and stored somewhere that can be referred from thread 2's scope. In case of Hibernate, the current org.springframework.orm.hibernate4.SessionHolder should be enough as it also keeps the link to the ongoing transaction.
  • Then, the thread 2 can be started with the following logic integrated at the very beginning of its execution: Binding all thread local variables retrieved in the first step to the same variables on thread 2, using Spring's utilities like org.springframework.transaction.support.TransactionSynchronizationManager.bindResource(Object, Object). After this, the second thread should be able to work in the same Hibernate session and transaction as its parent.

There might be subtle detail that we need to care but the following code should get the job done:

final SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.unbindResource(sessionFactory);
Runnable newTask = new Runnable() {

        @Override
        public void run() {
            TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
            // Business stuff here.
        }
    };
Executors.newFixedThreadPool(1).submit(newTask);

Hope this helps.

Dhruv answered 7/5, 2015 at 2:56 Comment(4)
Hm. I completely missed bindResource(). Do you have a code example how to transfer the transaction context (i.e. all thread locals in TransactionSynchronizationManager) from one thread to another?Caddaric
Interesting. I am unable to test it at the moment due to broken project but I will get back when I have tested this, it might take a day or two.Ferdinandferdinanda
It also works with other frameworks like Mybatis. If we look at TransactionSynchronizationManager.resourceMap in the main thread, we'll find an entry for SqlSessionFactory mapping a SqlSessionHolder. In any new thread, we just need to bind this SessionHolder to the same SessionFactory. In my case, I'm using parallelStream, so I added an additional check in the thread (the stream consumer) to do the binding only if TransactionSynchronizationManager.isActualTransactionActive() is false, because the thread pool can reuse at some point the main thread for which the transaction is configured yet.Unstained
This does work but in this case you are not able to have multi threaded database connections. When you copy these variables over, it is referencing the same connection.Preliminary
F
3

I have decided to submit my own answer that allows for this to happen in a different way, in all environments and frameworks, by having other threads delegate the work to an originating thread.

Example:

public static void main(String[] args) throws Exception {

        System.out.println(Thread.currentThread().getId() + ": (1)");

        JoinLock lock = new JoinLock();
        new Thread(() -> {
                lock.delegate(() -> {
                        System.out.println(Thread.currentThread().getId() + ": (a) ");  // Will execute in the originating thread
                });
        }).start();
        lock.await();

        System.out.println(Thread.currentThread().getId() + ": (2)");
}

Outputs:

1: (1)
1: (a) 
1: (2)

Another example using a passed runnable and some more details:

public static void main(String[] args) throws Exception {

        System.out.println(Thread.currentThread().getId() + ": (1)");

        new JoinLock().await((lock) -> {

                new Thread(() -> {

                        // Should execute as another thread
                        System.out.println(Thread.currentThread().getId() + ": (a) ");

                        // Will execute in the originating thread
                        lock.delegate(() -> {
                                System.out.println(Thread.currentThread().getId() + ": (b) ");

                                sleep(2000);

                                System.out.println(Thread.currentThread().getId() + ": (c) ");
                        });

                        // This should execute first when the delegate has finished executed, so after 1 : (c) but since it's a separate thread,
                        // ofcourse the originating thread might also execute prior, which happens because of our sleep here
                        sleep(2000);
                        System.out.println(Thread.currentThread().getId() + ": (d) ");

                }).start();

        });

        System.out.println(Thread.currentThread().getId() + ": (2)");

}

private static void sleep(long millis) {
        try {
                Thread.sleep(millis);
        } catch (InterruptedException e) {
                e.printStackTrace();
        }
}

Outputs:

1: (1)
13: (a) 
1: (b) 
1: (c) 
1: (2)
13: (d) 

Using an executor it would something like this:

new JoinLock().await((lock) -> {
        Executors.newFixedThreadPool(1).submit(() -> {
                lock.delegate(runnable);
        });
});

The lock used in the example can be found here:

GitHub: Opensource repository. Have a look at: JoinLock.java which also uses: SimpleLock.java.

Ferdinandferdinanda answered 7/5, 2015 at 10:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.