1. Nine major transaction failure scenarios

First, let’s look at the actual process address of the transaction

The database does not support transactions

For example, the MyISAM engine in MySQL databases does not support transactions.

The proxy class is not managed by Spring

From the source position org. Springframework. Aop) framework. Autoproxy. AbstractAutoProxyCreator# postProcessAfterInitialization

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean ! = null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) ! = bean) { return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; }Copy the code

If Spring does not manage it, it will not be able to create the proxy and the transaction will not take effect.

The proxied class was prematurely instantiated

In the last article, we talked about the address. If the Bean is instantiated too early, it may not be handled by the proxy postprocessor during the initialization process. Such as BeanDefinitionRegistryPostProcessor implementation class using the beanFactory. RegisterSingleton (” test “, new test ()); Register the Bean without even having a chance to initialize it.

For example, dependency injection in a post-processor:

@Component public class TestBeanPostProcessor implements BeanPostProcessor, Ordered { @Autowired private IAiExtractService aiExtractService; @Override public int getOrder() { return 1; }}Copy the code

Note: This problem can only occur if the custom postprocessor is executed before the AbstractAutoProxyCreator postprocessor, which in turn only implements BeanPostProcessor.

Why does dependency injection agent invalidate before AbstractAutoProxyCreator? Since Spring first registers the post-handler and then initializes the remaining beans, dependency injection initializes the beans during the registration process, which calls the existing post-handler in the container. The post-handler is not proxied until it is registered. See this address for details

1.5. Note that the starting point of the transaction annotation is not within the scope of the exception thrown

Some people may wonder why transaction annotations are generally annotated at the Service layer and not at the Controller layer. Do annotations in the control layer invalidate transactions?

No, no, no, because normally business logic is placed in the Service layer, and from the invocation of the method, the annotation of the transaction is the beginning of the valid scope of the transaction, and since most of the logic is in the Service layer, the controller is basically one line of code, and generally does not throw exceptions. Natural annotations are fine for controllers and services.

Most importantly, code in the Service layer may provide common calls. If my parent does not have transaction annotations and my annotations are in the Controller layer, exceptions thrown by the Service layer cannot be rolled back.

The starting point of the transaction is called by this, without really calling the proxy class

Let’s say I have some code that looks like this

public void test1(){
        test2();
    }
 
    @Transactional(rollbackFor = Exception.class)
    public void test2(){
        throw new RuntimeException();
    }
Copy the code

A normal transaction agent is a dynamic proxy class that generates a try catch around the method that calls the target, and rolls back if an exception occurs. However, at the beginning of the transaction, the target method is called using this, that is, using the real class to call the target method, the target method exception, naturally cannot be rolled back. You need to inject the current object, then use the proxy class to call the target method.

An exception thrown by a method is caught within the method and is not intercepted by a transaction interceptor

You can find the org. Springframework. Transaction. The interceptor. TransactionAspectSupport# invokeWithinTransaction this method, the specific code is executing a transaction.

try {
                // This is an around advice: Invoke the next interceptor in the chain.
                // This will normally result in a target object being invoked.
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                // target invocation exception
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
                cleanupTransactionInfo(txInfo);
            }
            commitTransactionAfterReturning(txInfo);
Copy the code

This line is the code to invoke the target method retVal = invocation. ProceedWithInvocation (); . If you are captured the exception, is certainly go directly to the commitTransactionAfterReturning commit the transaction.

The thrown exception does not match the exception that the transaction can handle

Follow the code above:

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { if (txInfo ! = null && txInfo.getTransactionStatus() ! = null) { if (logger.isTraceEnabled()) { logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex); } if (txInfo.transactionAttribute ! = null && txInfo.transactionAttribute.rollbackOn(ex)) { try { txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { logger.error("Application exception overridden by rollback exception", ex); ex2.initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { logger.error("Application exception overridden by rollback exception", ex); throw ex2; } } else { // We don't roll back on this exception. // Will still roll back if TransactionStatus.isRollbackOnly() is true. try { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { logger.error("Application exception overridden by commit exception", ex); ex2.initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { logger.error("Application exception overridden by commit exception", ex); throw ex2; } } } } public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); }Copy the code

We find that it only decides if it’s a subclass of RuntimeException or Error, otherwise it just commits the transaction. We know that there are also horizontal exceptions in addition to RuntimeException SQLException, IOException, If one of their subclasses is abnormal the natural transaction will not be rolled back.

Therefore, ali’s code specification requires that the rollback type of Exception must be named, because the specified Exception, if the specified level of the Exception will be rolled back, I have detailed explanation in the public number will not repeat.

Transaction manager not configured

The other is the invalidation of transactions caused by incorrect use of propagation behavior, which we will examine in the next section.

Last but not least, do you think the following transactions will be invalid?

@Autowired private ICallbackService callbackService; @Override @Transactional(rollbackFor = Exception.class) public void test1() { callbackService.test2(); } @override public void test2(){basemapper.insert (new CallbackEntity().setContent("").setAccountid (1)); Basemapper.insert (new CallbackEntity()); }Copy the code

It’s actually not invalidated. As mentioned above, test1 is the valid starting point of the transaction from the time of the invocation. Test2 throws an exception, and because it’s a RuntimeException, the exception will be thrown up and up and eventually handled by Test1’s transaction.