Spring transaction monitoring, why does the transaction fail?

Spring transaction monitoring, why does the transaction fail?

Enter my website to get the best reading experience


Spring provides @TransactionlEventListener annotations after version 4.2, which can easily do some processing after the transaction is submitted, but if it is used improperly or the operating logic behind it is not properly understood, it is easy to step on the pit or even cause online failures.

I encountered a problem in my previous work. During transaction monitoring, some transaction operations were performed, but the transaction did not take effect.

Today we will take a closer look at how this problem arises and how to solve it.

Problem recurrence

Let's simulate a very simple scenario: when an order is created, the "Order has been registered" event will be released, the operation record will be saved in the event monitor, and then the "Operation record has been saved" event will be released, and finally some things will be done in this event monitor. logic.

Some unimportant implementations are omitted in the following code.

The first is OrderService, where the order record is saved in the createOrder() method, and the "order has been registered" event is released:

public class OrderService {

    @Transactional
    public void createOrder() {
        String orderNo = "test_no";
        Order order = new Order(orderNo);
        orderRepository.save(order);
        log.info("publish OrderCreatedEvent");
        applicationContext.publishEvent(new OrderCreatedEvent(orderNo));
    }

}

 

In the event monitor of "Order has been registered", call operationService.saveOperation():

public class OrderCreatedEventListener {

    @TransactionalEventListener
    public void handle(OrderCreatedEvent event) {
        log.info("handle OrderCreatedEvent : " + event.getOrderNo());
        operationService.saveOperation(event.getOrderNo(), " ");
    }

}

 

OperationService.saveOperation(), save the operation record, and publish the "operation record has been saved" event:

public class OperationService {

    @Transactional
    public void saveOperation(String orderNo, String info) {
        Operation operation = new Operation(orderNo, info);
        operationRepository.save(operation);
        log.info("publish OperationSavedEvent");
        applicationContext.publishEvent(new OperationSavedEvent(orderNo, info));
    }

}

 

In the event monitor of "Operation record has been saved", print the log to replace the follow-up operation:

public class OperationSavedEventListener {

    @TransactionalEventListener
    public void handle(OperationSavedEvent event) {
        log.info("handle OperationSavedEvent : " + event.getOrderNo());
    }

}

 

To start the test, call the orderService.createOrder() method and look at the log print:

Hibernate: insert into order_entity (id, order_no) values (null, ?)
INFO c.l.s.service.OrderService    : publish OrderCreatedEvent
INFO c.l.s.event.OrderCreatedEventListener    : handle OrderCreatedEvent : test_no
INFO c.l.s.service.OperationService: publish OperationSavedEvent

 

Something strange happened! Only the order data is written in the database, no operation records are written, and after the OperationSavedEvent event is released, the listener callback is not executed.

Troubleshooting

First read about the official document, transaction events within chapters, has this to say tips:

The last sentence means: in the transaction event monitoring, there are no more transactions to join.

Looking back at the problem code above, OrderService.createOrder() is a transaction method. After the transaction is submitted, the OperationSavedEventListener is triggered. In this monitoring method, OperationService.saveOperation() is also a transaction method. The propagation type is the default, that is, it will Join the current transaction.

But when the saveOperation() is executed, the previous transaction has already been committed, so there is no way to join, resulting in the transaction of the operation record being not actually executed. And because the transaction saved by the operation record was not executed, the OperationSavedEventListener was not triggered.

Oh~ I probably understand the problem, let's enter the Spring source code to see if it is really the case.

First adjust the log level of JPA to debug

logging.level.org.springframework.orm.jpa=debug
 

Run it again and see the log:

DEBUG o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [co.lilpilot.springtestfield.service.OrderService.createOrder]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(1115296438<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@fe87ddd]
DEBUG o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1115296438<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
Hibernate: insert into order_entity (id, order_no) values (null, ?)
INFO c.l.s.service.OrderService    : publish OrderCreatedEvent
DEBUG o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
DEBUG o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1115296438<open>)]
INFO c.l.s.event.OrderCreatedEventListener    : handle OrderCreatedEvent : test_no
DEBUG o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1115296438<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
DEBUG o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1115296438<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
INFO c.l.s.service.OperationService: publish OperationSavedEvent
DEBUG o.s.orm.jpa.JpaTransactionManager        : Cannot register Spring after-completion synchronization with existing transaction - processing Spring after-completion callbacks immediately, with outcome status 'unknown'
DEBUG o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(1115296438<open>)] after transaction
 

Note that a log message appears: "Cannot register Spring after-completion synchronization with existing transaction-processing Spring after-completion callbacks immediately, with outcome status'unknown'".

Follow the vine to enter the JpaTransactionManager class, in fact, the printing of this log line is in its abstract parent class, namely AbstractPlatformTransactionManager.registerAfterCompletionWithExistingTransaction()

You can see that the transaction status is specified as STATUS_UNKNOWN, so transaction operations are no longer performed in the subsequent callback logic. This method is called in AbstractPlatformTransactionManager.triggerAfterCompletion():

The status of the transaction is judged here. At this time, our transaction status is that there is a transaction, but it is not a new transaction, so we entered the second judgment branch. The triggering place is AbstractPlatformTransactionManager.processCommit(), which is the place where Spring handles transaction submission:

private void processCommit(DefaultTransactionStatus status) throws TransactionException {
    try {

       //...   doCommit  

        try {
            triggerAfterCommit(status);
        }
        finally {
           // 
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
        }

    }
    finally {
       // 
        cleanupAfterCompletion(status);
    }
}
 

After the commit logic processing is completed, the position marked triggers the callback after the transaction is committed.

Seeing this, the problem is very clear. After the transaction is committed, Spring will trigger the subsequent callback logic, but if there is also a transaction method in the callback logic, but it is not a new transaction, the delusional transaction will not be submitted.

problem solved

In fact, if you understand the problem, the solution is naturally very simple. You only need to adjust the transmission type of the transaction, and mark the method of saving operation records as a new transaction:

public class OperationService {

    @Transactional(Transactional.TxType.REQUIRES_NEW)
    public void saveOperation(String orderNo, String info) {
        Operation operation = new Operation(orderNo, info);
        operationRepository.save(operation);
        log.info("publish OperationSavedEvent");
        applicationContext.publishEvent(new OperationSavedEvent(orderNo, info));
    }

}
 

In this way, the saving of operation records can be written to the database, and subsequent event monitoring can also be triggered.

One More Thing

Wait a minute, let s recall that Spring s event monitoring mechanism is actually a synchronous callback based on the observer pattern, and transaction event monitoring is the same, after the transaction is submitted, the callback that has been registered in the transaction synchronization register is obtained, Synchronous execution.

Just analyzed AbstractPlatformTransactionManager.processCommit(), after triggering the callback method triggerAfterCompletion(), there is the last step of operation cleanupAfterCompletion(), that is, the location of mark .

In this step, the database connection will be closed.

Did you realize something?

If the synchronization processing of transaction event monitoring is a time-consuming operation, the database connection will always be held. If there are a large number of concurrent calls online, the database connection pool is easily exhausted.

To solve this problem, you can consider asynchronous, use a new thread to handle this time-consuming call, end the callback early and release the previous database connection.

summary

In this article, we analyzed the transaction failure problem caused by the original transaction has been committed and subsequent transactions failed to join when using Spring's transaction listener. The solution is to treat the subsequent transaction as a new transaction.

At the same time, I sorted out the process of Spring transaction submission and subsequent processing, and understood that the callback operation still holds the previous database connection. If it takes too long, it may exhaust the connection pool. This problem can be avoided through new thread processing.