Transaction Problems in Springboot Source Analysis

Keywords: Java Spring Attribute shell Programming

Summary:

Transactions are ubiquitous in back-end development, which is the most basic guarantee of data consistency. Understand that the essence of incoming transactions is to go into the proxy method of transaction aspect. The most common is that the non-transactional method of the same class calls a method annotated with transaction and does not enter the transaction. We take cglib proxy as an example. Because of Spring's implementation of cglib AOP proxy, when entering the proxy method, we have actually left the "proxy shell". We can think that the code has come to a simple bean. Calling the method in the same bean naturally has no half-money relationship with the proxy.
Generally, declarative transactions are entered by calling a public method annotated with @Transactional in another class.

Key Processing Processes for spring Transactions

  • Enable Transaction Management annotation imports Transaction Management Configuration Selector
  • Transaction Management Configuration Selector loads Infrastructure Advisor AutoProxy Creator (but not necessarily it, usually Annotation Aware Aspect JAuto Proxy Creator), BeanFactory Transaction Attribute Source Advisor, Transaction Interceptor
    - Annotation Aware Aspect JAuto Proxy Creator is a key step in the ioc process to find Advisor. There are two aspects. The first is to implement the class of Advisor interface, and the second is to annotate Aspectj. The key is that the BeanFactory Transaction Attribute Source Advisor is loaded into the proxy cache
  • When a proxy calls a method, it executes DefaultAdvisor ChainFactory # getInterceptors AndDynamicInterception Advice, and then we will

BeanFactory Transaction Attribute Source Advisor is useful, but the most important thing is that it matches the Transaction Attribute Source Pointcut and executes the method of Transaction Interceptor.

TransactionInterceptor

    @Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
       // Work out the target class: may be {@code null}.
       // The TransactionAttributeSource should be passed the target class
       // as well as the method, which may be from an interface.
       Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    
       // Adapt to TransactionAspectSupport's invokeWithinTransaction...
       return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
    }

TransactionAspectSupport

    @Nullable
    protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
          final InvocationCallback invocation) throws Throwable {
    
       // If the transaction attribute is null, the method is non-transactional.
       TransactionAttributeSource tas = getTransactionAttributeSource();
       final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
       final PlatformTransactionManager tm = determineTransactionManager(txAttr);
       final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    
       if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
          // Standard transaction demarcation with getTransaction and commit/rollback calls.
          TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
    
          Object retVal;
          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);
          return retVal;
       }
    
       else {
          final ThrowableHolder throwableHolder = new ThrowableHolder();
    
          // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
          try {
             Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, status -> {
                TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
                try {
                   return invocation.proceedWithInvocation();
                }
                catch (Throwable ex) {
                   if (txAttr.rollbackOn(ex)) {
                      // A RuntimeException: will lead to a rollback.
                      if (ex instanceof RuntimeException) {
                         throw (RuntimeException) ex;
                      }
                      else {
                         throw new ThrowableHolderException(ex);
                      }
                   }
                   else {
                      // A normal return value: will lead to a commit.
                      throwableHolder.throwable = ex;
                      return null;
                   }
                }
                finally {
                   cleanupTransactionInfo(txInfo);
                }
             });
    
             // Check result state: It might indicate a Throwable to rethrow.
             if (throwableHolder.throwable != null) {
                throw throwableHolder.throwable;
             }
             return result;
          }
          catch (ThrowableHolderException ex) {
             throw ex.getCause();
          }
          catch (TransactionSystemException ex2) {
             if (throwableHolder.throwable != null) {
                logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                ex2.initApplicationException(throwableHolder.throwable);
             }
             throw ex2;
          }
          catch (Throwable ex2) {
             if (throwableHolder.throwable != null) {
                logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
             }
             throw ex2;
          }
       }
    }

This time in the analysis of this method, but from the perspective of business anomalies, ineffective to analyze the problem. Annotation transaction and programming are the same core idea, let's analyze the annotation transaction logic.

    if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
                // Standard transaction demarcation with getTransaction and commit/rollback calls.
                TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
    
                Object retVal;
                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 {
            // Rebind TxInfo from the previous transaction to ThreadLocal
                    cleanupTransactionInfo(txInfo);
                }
                commitTransactionAfterReturning(txInfo);
                return retVal;
            }

Keep in mind the logical sequence and exception capture of these core methods.

    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);
                }
          //Exception support for transaction rollback
                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;
                    }
                }
            }
        }

Exception support for transaction rollback

    @Override
    public boolean rollbackOn(Throwable ex) {
       return (ex instanceof RuntimeException || ex instanceof Error);
    }

Note that only runtime exceptions and error mechanisms are supported, otherwise no rollback is allowed. And the direct conditions.

AbstractPlatformTransactionManager

    private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
       try {
         //Default false
          boolean unexpectedRollback = unexpected;
          try {
            //Callback the beforeCompletion method of the TransactionSynchronization object.
             triggerBeforeCompletion(status);
             if (status.hasSavepoint()) {
                if (status.isDebug()) {
                   logger.debug("Rolling back transaction to savepoint");
                }
                status.rollbackToHeldSavepoint();
             }
            // Rolling back at the outermost transaction boundary
             else if (status.isNewTransaction()) {
                if (status.isDebug()) {
                   logger.debug("Initiating transaction rollback");
                }
               // Rollback is implemented by specific TxMgr subclasses.
                doRollback(status);
             }
             else {
                // Participating in larger transaction
                if (status.hasTransaction()) {
                  /*
                     * When the underlying transaction is marked rollBackOnly or the global Rollback OnParticipation Failure switch is turned on, the current transaction tag needs to be rolled back.
                     * 
                     * If the underlying transaction is explicitly marked rollBackOnly, the whole transaction must eventually be rolled back.
                     * 
                     * But if rollBackOnly is not marked, the global Rollback OnParticipation Failure switch is important.
                     * globalRollbackOnParticipationFailure The switch is turned on by default, that is to say, the internal transaction hangs up, and the final result is only a full transaction rollback.
                     * But if the global Rollback On Participation Failure switch is turned off, the inner transaction hangs, and the outer transaction business method can control whether to roll back according to the situation.
                     */
     
                   if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                      if (status.isDebug()) {
                         logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                      }
                     // Rollback is implemented by specific TxMgr subclasses.
                      doSetRollbackOnly(status);
                   }
                   else {
                      if (status.isDebug()) {
                         logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
                      }
                   }
                }
                else {
                   logger.debug("Should roll back transaction but cannot - no transaction available");
                }
                // Unexpected rollback only matters here if we're asked to fail early
                if (!isFailEarlyOnGlobalRollbackOnly()) {
                   unexpectedRollback = false;
                }
             }
          }
          catch (RuntimeException | Error ex) {
             triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
             throw ex;
          }
    // Callback the afterCompletion method of the TransactionSynchronization object.
          triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
    
          // Raise UnexpectedRollbackException if we had a global rollback-only marker
          if (unexpectedRollback) {
             throw new UnexpectedRollbackException(
                   "Transaction rolled back because it has been marked as rollback-only");
          }
       }
       finally {
          cleanupAfterCompletion(status);
       }
    }

case analysis

Experienced students must know that the whole transaction was eventually rolled back, and TransactionB_ test did not execute System. out. println ("TransactionB_ test after");
In fact, for Spring transactions, this result is correct, but for developers, this result does seem to be somewhat "incomprehensible".

We might as well analyze the reasons:

First, TransactionB#test itself throws RuntimeException directly, so when you go back to the transaction aspect, the transaction aspect will find that you need to roll back, but because TransactionB#test is not the outermost boundary of the transaction, the AbstractPlatform Transaction Manager# processRollback method only calls doSetRollback status. Subclass DataSourceTransaction Manager will take out the transaction object in DefaultTransaction Statues and mark it with rollback, specifically the transaction object (type DataSourceTransaction Object for DataSourceTransaction Manager) will take out the Connection Holder and call setRollbackOnly. We know that this is equivalent to markup being a global markup, because a Spring transaction belonging to the same physical transaction can read the same Connection Holder.

    protected void doSetRollbackOnly(DefaultTransactionStatus status) {
            DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)status.getTransaction();
            if (status.isDebug()) {
                this.logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + "] rollback-only");
            }
      //key point
            txObject.setRollbackOnly();
        }

Back to the upper transaction aspect, read in the AbstractPlatform Transaction Manager commit method that if (! ShouldCommitOnGlobalRollbackOnly () & & defStatus. isGlobalRollbackOnly () condition is valid, then call processRollback, because it will physically roll back at the outermost boundary of the transaction, and it is also at the outermost boundary of the transaction. Spring throws UnexpectedRollbackException.

How to solve it?

Then how to solve the problem? There are several ways to solve the problem, but it has to be decided according to the specific situation.

  • If the embedded transaction annotation is cancelled, Spring will not throw an Unexpected Rollback Exception, depending on the actual code and business situation. However, the method is not fully implemented, so such a solution can easily lead to incomplete dirty data.

  • Manual control to roll back. If you can't accept embedded transaction hanging, you can add TransactionAspectSupport. CurrtTransactionStatus (). setRollbackOnly () to the catch block to explicitly control rollback. So Spring knows that you want to roll back the transaction, not unexpected. Spring will not throw Unexpected Rollback Exception. So if an exception is caught in the upper transaction, you really don't want to roll back. Even if an exception occurs in the upper transaction, you want to commit the whole transaction eventually? If such a requirement exists, you can configure the transaction manager with a parameter setGlobalRollbackOnParticipation Failure (false);

  • If isGlobal Rollback OnParticipation Failure is false, the primary transaction decides to roll back. If an exception fails to join the transaction, the caller can continue to decide whether to roll back or continue within the transaction. However, it should be noted that this method can only be applied in the case of data access failure and can be solved as long as all operational transactions can be submitted, but it obviously affects the global transaction attributes, so it is strongly not recommended.

    public final void commit(TransactionStatus status) throws TransactionException {
       if (status.isCompleted()) {
          throw new IllegalTransactionStateException(
                "Transaction is already completed - do not call commit or rollback more than once per transaction");
       }
    
       DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
       if (defStatus.isLocalRollbackOnly()) {
          if (defStatus.isDebug()) {
             logger.debug("Transactional code has requested rollback");
          }
          processRollback(defStatus, false);
          return;
       }
    
       if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
          if (defStatus.isDebug()) {
             logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
          }
          processRollback(defStatus, true);
          return;
       }
    
       processCommit(defStatus);
    }

Posted by HIV on Thu, 05 Sep 2019 01:37:50 -0700