简体   繁体   中英

Spring multiple transactions with TransactionManager (JMS, database)

I have a method where I want to execute two transactions: one with DB and one with JMS. And I want one to commit after another. I'm trying to use PlatformTransactionManager for that. There are 2 ways of doing that: using TransactionTemplate or DefaultTransactionDefinition . But I didn't find a single example of usage any of that multiple times. What I want to do is:

void do(){
 T dbTransaction = ...; // here goes: new TransactionTemplate(transactionManager) two times
 T jmsTransaction = ...; // or: new DefaultTransactionDefinition() and transactionManager.getTransaction(definition); two times
 saveDb();
 sendJms();
 dbTransaction.commit();
 jmsTransaction.commit();
}

But I'm not sure what to use and how, because in this article it says that:

Anyway, once we create a TransactionTemplate with a configuration, all transactions will use that configuration to execute. So, if we need multiple configurations, we should create multiple template instances.

So how do I correctly create two transcations and close one after each other? Should I create two separate definitions or I can reuse one? Can I reuse same transactionManager in two templates ? I know that there's a @Transcational annotation for DB and that I can configure JMS to use transcations too, but:

  1. I didn't found good examples how to configure JMS for using transactions
  2. I'm not sure in which oreder they will close

So I want to manually do that. I'm also not sure that this manual transaction will work with JMS (for example, IBM-MQ) because I only saw examples of transactions for databases.

It's unclear why exactly you wish to use JMS transactions in this particular case and I'd even argue against it - at least as you have presented them above.

You basically want to publish a message once the state has been successfully stored to the Database.

Since your source of truth is the database, why not base all subsequent actions off of that action being successfully completed?

For example, one way to build this would be something like (Spring-oriented since you've mentioned that you're using it):

  1. Create a JmsSender bean which is scoped to the current transaction. This can be done by implementing a BeanFactoryPostProcessor and doing something like:
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        SimpleTransactionScope transactionScope = new SimpleTransactionScope();
        // The scope exists, but is not registered by Spring by default.
        beanFactory.registerScope("transaction", transactionScope);
    }

    // in a separate configuration class defining your JmsSender bean
    @Bean
    @Scope("transaction")
    public JmsSender jmsSender() { return new JmsSender(); }
  1. Every time the send() method of this bean is called, a message is added to an internal queue. This is usually a ThreadLocal<List<T>> - in fact, Spring handles transaction management pretty much the same way.
  2. Create a AfterCommitJmsPublisher bean which is a TransactionSynchronizationAdapter - this means that we want additional behaviour on commit.
  3. Register the AfterCommitJmsPublisher . This means calling TransactionSynchronizationManager.registerSynchronization(jmsPublisher) prior to the transaction. One way to do this, using eg Aspects, declarative transaction management ( @Transactional ) and Spring AOP would be:
@Aspect
@Component
public class AfterCommitJmsPublisher extends TransactionSynchronizationAdapter {

    private final JmsPublisher;

    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    private void transactionalPointcut() {
    }

    @Before("transactionalPointcut()")
    public void registerTransactionSynchronization() {
        TransactionSynchronizationManager.registerSynchronization(this);
    }
  1. When your Database transaction has been committed, call something like jmsPublisher.publish() . This can be done in the afterCommit() Method of your TransactionSynchronizationAdapter :
    // In AfterCommitJmsPublisher
    @Override
    public void afterCommit() {
        jmsPublisher.publish();
    }
  1. If the transaction was rolled back, then call something like jmsPublisher.clear() . You probably do not want to publish any messages concerning a failed action.

This way, your JMS messages are always bound to the transaction from which they originated - if the database transaction failed, no messages will be sent.

Going off your comment:

In the manual scenario it will fail if JMS failed, but will save it to DB if there's no exception with JMS, and even if after that transaction with JMS will fail I'm ok with that because I'm saving the state in DB.

This would likely suffice for your requirements. However, you might want to take into account that the more components you have, the more fault-tolerant your system needs to be and account for external services potentially not being available.

This could mean saving the JMS messages in a special Database table (as part of the transaction,) and only publishing after a successful commit. deleting the saved messages after a successful publish, If it was unsuccessful. you could implement a housekeeper task that reattempts the publication of your messages.

Lastly, a word on distributed transactions: personally I would advise against using them if at all possible, especially for your current use case. They are complex beasts which almost surely impact the availability of your application and increase end-to-end latency of all the processes which are involved in the transaction. Something like the Saga pattern is usually a far better fit for a distributed system.

Of course, this might not be applicable to your use case and your consistency requirements might outweigh any availability requirements, so take it with a grain of salt.

Your use case is simple and common. You wish to send a JMS message, wait for that to complete, then commit to the database. This is done on two transactions - one for the JMS message and the other for the database. These transactions both exist in a single transaction context. When you start the JMS transaction, no transaction context will exist, so one will be created. When you start the database transaction, it will join the existing transaction context. These transaction will be synchronized in that the JMS transaction must successfully complete before the database transaction is committed.

Central to this operation is the transaction manager. Looking at the article that you linked, they make many references to the PlatformTransactionManager . In your use case, the PlatformTransactionManager must be a JTA capable transaction manager. A JTA transaction manager will be able to create the transaction context and register and synchronize the transactions.

Note that these are two local transactions, this is not in any way XA or distributed transactions. In this scenario, if the JMS local transaction fails, then the database local transaction will be rolled back. More specifically, it is the transaction context that gets marked as rollback only. If any unhandled exception occurs, then the transaction context is marked rollback only. Any attempts to call commit() on the local transactions will fail with a message stating that the transaction context is rollback only.

Achieving this is platform dependent. For example, if your Spring project is deployed in a WAR file on an application server such as JBoss, then the PlatformTransactionManager will be autowired automatically. If you are using Spring Boot, then most configurations do not even include a transaction manager.

I have a transactional Spring JMS and Camel for Spring Boot here . This is a simple message bridge for IBM MQ. If nothing else, the Spring JMS and annotations and the example for transactional IBM MQ should be useful. Maybe the Camel bits are useful as well.

Note that the pom.xml file contains:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jta-narayana</artifactId>
    </dependency>

This Spring Boot starter will install and configure the Arjuna JTA transaction manager as the PlatformTransactionManager .

In my example, I have:

<logger name="com.arjuna" level="TRACE" additivity="false">
    <appender-ref ref="STDOUT" />
</logger>

This provides very nice logging for the Arjuna JTA transaction manager.

Bottom line, get a JTA transaction manager configured as the PlatformTransactionManager . Use this to create a transaction context, and have the two local synchronized transactions in that context.

The example project should be easy to get to run. The logging is very informative.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM