Detailed Explanation of Spring Transaction Implementation Principle

Keywords: Spring Attribute JDBC Hibernate

Above( Tangent Analysis of Spring Transactions ) In this article, we explain how Spring determines whether the target method needs to weave into aspect logic, which explains that transaction logic is woven through Transaction Interceptor. This article mainly explains how Transaction Interceptor weaves into aspect logic.

1. Global Transaction Logic

Spring implements aspect logic weaving through Aop, where the Transaction Interceptor implements the Method Interceptor interface, which inherits the Advice interface, that is to say, the Transaction Interceptor is essentially only part of the aspect logic that Spring Aop needs to weave into. The following is the source code for the invoke() method implemented by Transaction Interceptor:

@Override
@Nullable
public Object invoke(final MethodInvocation invocation) throws Throwable {
    // Get the target class that needs to be woven into transaction logic
    Class<?> targetClass = (invocation.getThis() != null ? 
        AopUtils.getTargetClass(invocation.getThis()) : null);

    // Weaving transaction logic
    return invokeWithinTransaction(invocation.getMethod(), targetClass, 
        invocation::proceed);
}

Here the weaving of transaction logic is encapsulated in invokeWithin Transaction (), and we continue to read its source code:

@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
       final InvocationCallback invocation) throws Throwable {
    // Get the relevant attributes in @Transactional
    TransactionAttributeSource tas = getTransactionAttributeSource();
    final TransactionAttribute txAttr = (tas != null ? 
        tas.getTransactionAttribute(method, targetClass) : null);
    // Get the current Transaction Manager configuration, which is typically configured in the configuration file
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);
    // Get a signature of the current method
    final String joinpointIdentification = 
        methodIdentification(method, targetClass, txAttr);

    // If the configured Transaction Manager is not of the Callback Preferring Platform Transaction Manager type,
    // Create a new transaction for the execution of the current method
    if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
        // Create a new transaction for the execution of the current method
        TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, 
             joinpointIdentification);
        Object retVal = null;
        try {
            // Target implementation approach
            retVal = invocation.proceedWithInvocation();
        } catch (Throwable ex) {
            // Handling exceptions while executing throw exceptions and weaving exception handling logic
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        } finally {
            // Execute the logic of transaction completion, whether the transaction needs to be committed or rolled back
            cleanupTransactionInfo(txInfo);
        }
        // Submit the current transaction
        commitTransactionAfterReturning(txInfo);
        return retVal;
    } else {
        final ThrowableHolder throwableHolder = new ThrowableHolder();
        try {
            // If the current Transaction Manager implements Callback Preferring Platform Transaction Manager,
            // The transaction is processed through its execute() method. Here is Callback Preferring Platform-
            // The role of Transaction Manager is that it provides an execute() method for providing customization
            // Transaction Manager class implements transaction-related processing logic
            Object result = ((CallbackPreferringPlatformTransactionManager) tm)
                .execute(txAttr, status -> {
                // Get the Transaction configuration
                TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, 
                     joinpointIdentification, status);
                try {
                    // Call the target method
                    return invocation.proceedWithInvocation();
                } catch (Throwable ex) {
                    // If the current exception needs to be rolled back, the exception is rolled back and thrown
                    if (txAttr.rollbackOn(ex)) {
                        if (ex instanceof RuntimeException) {
                            throw (RuntimeException) ex;
                        } else {
                            throw new ThrowableHolderException(ex);
                        }
                    } else {
                        throwableHolder.throwable = ex;
                        return null;
                    }
                } finally {
                    // Clear saved Transaction information
                    cleanupTransactionInfo(txInfo);
                }
            });

            // If an exception is executed, the exception is thrown
            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;
        }
    }
}

As you can see, the backbone logic weaved by Spring transaction logic is in the invokeWithinTransaction() method, which first determines whether a transaction needs to be created and encapsulates it as a TransactionInfo object, regardless of whether it exists or not. Then the target method is invoked. If the target method performs error reporting, the operation when exception is thrown is performed, which mainly includes transaction rollback and exception callback logic. The current transaction attributes are then cleaned up. If the current transaction is a new transaction, it commits directly. If a transaction uses a savepoint, it releases the savepoint it holds and commits the transaction.

2. Create a transaction

When weaving transaction logic, the first thing to do is create transactions, and Spring does not create transactions at will. It determines what types of transactions need to be created by the configuration attributes of transactions. Transaction creation is mainly in the createTransactionIfNecessary() method. The source code of this method is as follows:

protected TransactionInfo createTransactionIfNecessary(
    @Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, 
    final String joinpointIdentification) {
    // If the name of the TransactionAttribute is empty, a TransactionAttribute of the agent is created.
    // And set its name to the name of the method that needs to be woven into the transaction
    if (txAttr != null && txAttr.getName() == null) {
        txAttr = new DelegatingTransactionAttribute(txAttr) {
            @Override
            public String getName() {
                return joinpointIdentification;
            }
        };
    }

    TransactionStatus status = null;
    if (txAttr != null) {
        if (tm != null) {
            // If the transaction attribute is not empty and Transaction Manager exists,
            // Obtain the object of the current transaction status through Transaction Manager
            status = tm.getTransaction(txAttr);
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Skipping transactional joinpoint [" 
                     + joinpointIdentification 
                     + "] because no transaction manager has been configured");
            }
        }
    }
    
    // Encapsulating the current transaction attributes and state as a TransactionInfo, the main task here is to bind the transaction attributes to the current thread
    return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

For transaction creation, the first step is to determine whether the object name encapsulating transaction attributes is empty, if not, to use the identifier of the target method as its name, and then to create a transaction through Transaction Manager. The following is the source code for the TransactionManager.getTransaction() method:

@Override
public final TransactionStatus getTransaction(
    @Nullable TransactionDefinition definition) throws TransactionException {
    // For Spring's BasicDataSource, this is to create a DataSourceTransactionObject object.
    // Its return value is different according to the specific implementation class, for example, hibernate returns a Hibernate Transaction Object object.
    // Jpa returns a JpaTransactionObject object
    Object transaction = doGetTransaction();
    boolean debugEnabled = logger.isDebugEnabled();

    // If Transaction Definition is empty, create a default Transaction Definition
    if (definition == null) {
        definition = new DefaultTransactionDefinition();
    }

    // The way to determine whether the current method call is already in a transaction is to determine whether the current ConnectionHolder is empty.
    // And whether the existing transaction is in active state or not, if so, it indicates that the current transaction exists. If there is no business here, generally,
    // Its Connection Holder is worthless
    if (isExistingTransaction(transaction)) {
        // Determine whether the transactional propagability of the current method supports the existing transactional attributes, and return the encapsulated transactional attributes
        return handleExistingTransaction(definition, transaction, debugEnabled);
    }

    // Here the default value of timeout is - 1, and the expiration time set by the user cannot be less than - 1.
    if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
        throw new InvalidTimeoutException("Invalid transaction timeout", 
            definition.getTimeout());
    }
    
    // If the execution of the current method is not in a transaction and the transaction propagability of the current method is marked PROPAGATION_MANDATORY,
    // Because this propaganda requires that the current method execution must be in a transaction, and that the transaction must exist, there does not exist, and so on.
    // throw
    if (definition.getPropagationBehavior() == 
        TransactionDefinition.PROPAGATION_MANDATORY) {
        throw new IllegalTransactionStateException("No existing transaction" 
            + " found for transaction marked with propagation 'mandatory'");
    } else if (definition.getPropagationBehavior() == 
               TransactionDefinition.PROPAGATION_REQUIRED 
        || definition.getPropagationBehavior() == 
               TransactionDefinition.PROPAGATION_REQUIRES_NEW 
        || definition.getPropagationBehavior() == 
               TransactionDefinition.PROPAGATION_NESTED) {
        // Determine whether the transaction propagability of the current method is one of REQUIRED, REQUIRES_NEW or NESTED.
        // Since the current method comes here to show that there is no transaction, a new transaction needs to be created for it. Here the suspend() method
        // When calling, a null is passed in. If the user sets the properties of the transaction event callbacks, they will be suspended temporarily.
        // And encapsulated in Suspended Resources Holder, if no callback event is registered, the method will return null
        SuspendedResourcesHolder suspendedResources = suspend(null);
        if (debugEnabled) {
            logger.debug("Creating new transaction with name [" 
                + definition.getName() + "]: " + definition);
        }
        try {
            // Determine whether the user has set properties that never execute transaction callback events
            boolean newSynchronization = 
                (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
            // Encapsulate the definition of current transaction attributes, existing transactions and pending transaction callback events into one
            // DefaultTransactionStatus object
            DefaultTransactionStatus status = newTransactionStatus(definition, 
                 transaction, true, newSynchronization, debugEnabled, suspendedResources);
            // Start a transaction by calling Jdbc's api
            doBegin(transaction, definition);
            // Set some attributes of the current transaction, such as isolation level, read-only or not, to the ThreadLocal variable for caching
            prepareSynchronization(status, definition);
            return status;
        } catch (RuntimeException | Error ex) {
            // If an exception is thrown by the current transaction, the pending transaction and transaction events are reloaded, because there are no transactions currently.
            // So the incoming transaction that needs to be loaded is null
            resume(null, suspendedResources);
            throw ex;
        }
    } else {
        if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT 
            && logger.isWarnEnabled()) {
            logger.warn("Custom isolation level specified but no actual transaction " 
               + "initiated; isolation level will effectively be ignored: " + definition);
        }
        // This step shows that transactions are SUPPORTS, NOT_SUPPORTED or NEVER because there are no transactions at present.
        // For these kinds of propaganda, they do not need transactions, so there is no need to do other processing, directly encapsulating an empty transaction.
        // Transaction Statues is OK
        boolean newSynchronization = 
            (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        return prepareTransactionStatus(definition, null, true, 
             newSynchronization, debugEnabled, null);
    }
}

Here the getTransaction() method can be divided into two main aspects: 1) the current method call already exists in a transaction; 2) there is no transaction before the current method call. For the first point, we will talk about below, and for the second point, there are three main branches of judgment: 1) if the transaction propagation is MANDATORY, then throw an exception, because it requires that there must be a transaction; 2) if the transaction propagation is REQUIRED, REQUIRES_NEW or NESTED, then create a new transaction for its use; 3) For other cases, namely SUPPORTS, NOT_SU. PPORTED and NEVER, because there is no transaction at present, can directly create a TransactionStatus with empty transaction to return. For the processing of existing transactions, it is mainly carried out in the handleExistingTransaction() method. The source code of this method is as follows:

private TransactionStatus handleExistingTransaction(TransactionDefinition definition, 
    Object transaction, boolean debugEnabled) throws TransactionException {
    // Going to this point indicates that a transaction already exists and throws an exception if the execution of the current method does not support an existing transaction
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
        throw new IllegalTransactionStateException(
            "Existing transaction found for transaction marked with propagation 'never'");
    }

    // For the propagability of the NOT_SUPPORTED type, if a transaction already exists, the transaction is suspended and the current method is guaranteed.
    // Execution is non-transactional; if there is no current transaction, it is not processed
    if (definition.getPropagationBehavior() == 
        TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
        if (debugEnabled) {
            logger.debug("Suspending current transaction");
        }
        // Suspend an existing transaction
        Object suspendedResources = suspend(transaction);
        // Determine whether the user has set a callback function to always execute transaction events
        boolean newSynchronization = 
            (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        // Encapsulating transaction results, where the incoming transaction is null because the current method requires no execution in the transaction, and for
        // Suspended transactions, which are subsequently reloaded, are passed in the suspended resources transaction attribute
        return prepareTransactionStatus( definition, null, false, newSynchronization, 
            debugEnabled, suspendedResources);
    }

    // If the propagation of the current transaction is REQUIRES_NEW, the current transaction is suspended, which requires that the current method must be in a new transaction.
    // Do it, so a new transaction will be created here, and then the transaction will be opened
    if (definition.getPropagationBehavior() == 
        TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
        if (debugEnabled) {
            logger.debug("Suspending current transaction, creating new transaction " 
                + " with name [" + definition.getName() + "]");
        }
        // Suspend the current transaction and transaction event callback function
        SuspendedResourcesHolder suspendedResources = suspend(transaction);
        try {
            // Determine whether the user has set a callback function that never executes transaction events
            boolean newSynchronization = 
                (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
            // Create a new transaction execution
            DefaultTransactionStatus status = newTransactionStatus(definition, 
                 transaction, true, newSynchronization, debugEnabled, suspendedResources);
            // Start a new business
            doBegin(transaction, definition);
            // Set properties such as isolation of new transactions, whether read-only or not to ThreadLocal
            prepareSynchronization(status, definition);
            return status;
        } catch (RuntimeException | Error beginEx) {
            // If an exception is thrown in a new transaction, the existing transaction is reloaded and executed
            resumeAfterBeginException(transaction, suspendedResources, beginEx);
            throw beginEx;
        }
    }

    // Determine whether the current transaction propaganda is NESTED, if so, and then determine whether the current data source supports nested transactions, if not,
    // Exceptions are thrown; if the data source specifies that nested transactions are executed using savepoints, nested transactions are simulated using savepoints.
    // If not specified, nested transactions are executed in a user-defined manner
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
        // If nested transactions are not supported, exceptions are thrown
        if (!isNestedTransactionAllowed()) {
            throw new NestedTransactionNotSupportedException(
                "Transaction manager does not allow nested transactions by default - " +
                "specify 'nestedTransactionAllowed' property with value 'true'");
        }
        if (debugEnabled) {
            logger.debug("Creating nested transaction with name [" 
                         + definition.getName() + "]");
        }
        
        // If nested transactions are specified using savepoints, a savepoint is created to execute the current method.
        if (useSavepointForNestedTransaction()) {
            DefaultTransactionStatus status =
                prepareTransactionStatus(definition, transaction, false, false, 
                    debugEnabled, null);
            // Create savepoints
            status.createAndHoldSavepoint();
            return status;
        } else {
            // If you do not specify a way to use savepoints, it is up to the user to use their own custom way, where only a new transaction will be opened.
            boolean newSynchronization = 
                (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
            DefaultTransactionStatus status = newTransactionStatus(
                definition, transaction, true, newSynchronization, debugEnabled, null);
            doBegin(transaction, definition);
            prepareSynchronization(status, definition);
            return status;
        }
    }

    if (debugEnabled) {
        logger.debug("Participating in existing transaction");
    }
    
    // At this point, the transaction propaganda is SUPPORTS or REQUIRED, both of which can only inherit existing transactions.
    // Execute the current method. Therefore, when inheriting, we will judge whether the transaction attribute of the existing method is in conflict with the transaction attribute of the current method. There
    // Judgment mainly includes two aspects: transaction isolation level and whether it is read-only or not. If the transaction isolation level and read-only attributes of the current method are integrated with the transaction
    // If inconsistent, an exception is thrown
    if (isValidateExistingTransaction()) {
        if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
            Integer currentIsolationLevel = 
                TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
            // Determine whether the transaction isolation level is consistent or inconsistent and throw an exception
            if (currentIsolationLevel == null 
                || currentIsolationLevel != definition.getIsolationLevel()) {
                Constants isoConstants = DefaultTransactionDefinition.constants;
                throw new IllegalTransactionStateException("Participating transaction " 
                    + " with definition [" + definition + "] specifies isolation " 
                    + " level which is incompatible with existing transaction: " 
                    + (currentIsolationLevel != null ?
                       isoConstants.toCode(currentIsolationLevel, 
                       DefaultTransactionDefinition.PREFIX_ISOLATION) : "(unknown)"));
            }
        }
        if (!definition.isReadOnly()) {
            // Determine whether read-only attributes are consistent, and if not, throw an exception
            if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
                throw new IllegalTransactionStateException("Participating transaction " 
                    + " with definition [" + definition + "] is not marked as " 
                    + " read-only but existing transaction is");
            }
        }
    }
    
    // Since both SUPPORTS and REQUIRED inherit the parent transaction to execute the current method, this is just a case of encapsulating the parent transaction and returning it.
    boolean newSynchronization = 
        (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    return prepareTransactionStatus(definition, transaction, false, newSynchronization, 
         debugEnabled, null);
}

The handleExistingTransaction() method here is mainly to determine whether a new transaction is needed in the case of an existing transaction. The basis of judgment here is mainly based on the transactional propaganda of the current method: (1) if the propaganda is NEVER, an exception will be thrown directly; (2) if the propaganda is NOT_SUPPORTED, the current transaction will be suspended and the current method will be executed in a non-transactional state; (3) if REQUIRES_NEW, the current transaction will be suspended and a new transaction will be created to execute the current method. If it is a nested transaction, it will determine whether the current data source supports the nested transaction and whether it needs to execute the nested transaction in the way of savepoints. _If it is SUPORTS or REQUIRED, it will directly reuse the current transaction execution target method.

3. Open a transaction

As far as transaction initiation is concerned, the doBegin() method here is concerned, where the main task of doBegin() is to open a database Connection and set autoCommit, isolation and readOnly attributes. It should be noted that the doBegin() method is a template method, and different Transaction Managers have different implementations. This paper mainly explains the implementation principle of DataSource Transaction Manager. The following is the source code for the doBegin() method:

@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
    // For DataSourceTransaction Manager, the transaction object is the DataSourceTransaction Object type.
    // So it can be forced directly here.
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    Connection con = null;

    try {
        // For the first open transaction, there is no Connection Holder, so the if logic will follow here.
        if (!txObject.hasConnectionHolder() ||
            txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            // Getting a database Connection through the DataSource object
            Connection newCon = obtainDataSource().getConnection();
            if (logger.isDebugEnabled()) {
                logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
            }
            // Create a new Connection Holder, where the second parameter indicates whether or not the new Connection Holder is created
            txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
        }

        // Setting Synchronization State
        txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
        con = txObject.getConnectionHolder().getConnection();

        // The main work of the prepareConnectionForTransaction() method here is to read readOnly and
        // isolation property and set it to ConnectionHolder if it already exists before
        // The isolation value, which is not the same as in definition, is set to previous Isolation Level
        Integer previousIsolationLevel = 
            DataSourceUtils.prepareConnectionForTransaction(con, definition);
        txObject.setPreviousIsolationLevel(previousIsolationLevel);
        // Set autoCommit to false to control transaction submission
        if (con.getAutoCommit()) {
            txObject.setMustRestoreAutoCommit(true);
            if (logger.isDebugEnabled()) {
                logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
            }
            con.setAutoCommit(false);
        }

        // Here, the readOnly attribute is set from the Statement level.
        prepareTransactionalConnection(con, definition);
        txObject.getConnectionHolder().setTransactionActive(true);

        // Determine whether the timeout attribute in definition has a valid value, and set it to ConnectionHolder if it exists
        int timeout = determineTimeout(definition);
        if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
            txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
        }

        // If a new Connection Holder is created, it is bound to the ThreadLocal object
        if (txObject.isNewConnectionHolder()) {
            TransactionSynchronizationManager.bindResource(obtainDataSource(), 
                txObject.getConnectionHolder());
        }
    } catch (Throwable ex) {
        // If an exception is thrown during the above execution and a new Connection Holder is created, the Connection is released
        if (txObject.isNewConnectionHolder()) {
            DataSourceUtils.releaseConnection(con, obtainDataSource());
            txObject.setConnectionHolder(null, false);
        } throw new CannotCreateTransactionException("Could not open JDBC Connection " 
            + " for transaction", ex);
    }
}

4. exception handling

For exception handling, Spring transactions can be divided into two situations: first, if the current exception can be omitted, it will be submitted directly; and second, if the current exception cannot be omitted, it will be rolled back. Specific implementation source code in the completeTransaction AfterThrowing () method, the following is the source code of the method:

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, 
          Throwable ex) {
    // Processing if transaction information exists and the transaction state is not empty
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        if (logger.isTraceEnabled()) {
            logger.trace("Completing transaction for [" 
               + txInfo.getJoinpointIdentification() + "] after exception: " + ex);
        }
        // If the current transaction configuration requires a rollback of the current exception type, it is rolled back.
        if (txInfo.transactionAttribute != null 
            && txInfo.transactionAttribute.rollbackOn(ex)) {
            try {
                // Here, when rolling back, we mainly use the Connection object to roll back.
                // In addition, transaction event functions are also invoked when rollback occurs.
                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 {
            try {
                // If the current exception is not a type that needs to be rolled back, it does not roll back and commits the current transaction directly.
                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;
            }
        }
    }
}

As you can see, the rollback operation here is mainly in the TransactionManager.rollback() method, the source code of which is as follows:

@Override
public final void rollback(TransactionStatus status) throws TransactionException {
    // If the current transaction has been completed, an exception is thrown
    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;
    // Transaction rollback
    processRollback(defStatus, false);
}

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
    try {
        boolean unexpectedRollback = unexpected;
        try {
            // This triggers the before completion time, and it's important to note that
            // Only when the transaction state is a newly created transaction event synchronizer will it be triggered
            triggerBeforeCompletion(status);

            // If the current transaction is a savepoint-type transaction, rollback to the savepoint
            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Rolling back transaction to savepoint");
                }
                status.rollbackToHeldSavepoint();
            } else if (status.isNewTransaction()) {
                // If the current transaction is a new transaction, rollback will occur at this time
                if (status.isDebug()) {
                    logger.debug("Initiating transaction rollback");
                }
                doRollback(status);
            } else {
                // If it is not for the above two cases, it means that the current transaction is in a large transaction, at which time the rollback state will be set.
                // Exceptions are thrown directly by external calls
                if (status.hasTransaction()) {
                    // If only partial rollback is set, or global exception rollback is set, rollback status is set.
                    if (status.isLocalRollbackOnly() 
                        || isGlobalRollbackOnParticipationFailure()) {
                        if (status.isDebug()) {
                            logger.debug("Participating transaction failed -" 
                                + " marking existing transaction as rollback-only");
                        }
                        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");
                }
                if (!isFailEarlyOnGlobalRollbackOnly()) {
                    unexpectedRollback = false;
                }
            }
        } catch (RuntimeException | Error ex) {
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
            throw ex;
        }

        // Trigger the after completion event, where only the newly created transaction event synchronizer will trigger
        triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
        if (unexpectedRollback) {
            throw new UnexpectedRollbackException(
                "Transaction rolled back because it has been marked as rollback-only");
        }
    } finally {
        // Clean up the transaction state saved in the current thread and reload the transaction if there is a pending transaction outside the current transaction
        cleanupAfterCompletion(status);
    }
}

For the rollback of transactions, it can be seen that only the outermost transaction (which will be marked as a new transaction) will roll back, while the savepoint transaction, or internal transaction, will not roll back, but only rollback to the savepoint or mark the rollback status.

5. Submission

For transaction submission, it is mainly in the commitTransaction AfterReturning () method, in which Spring first determines whether the transaction has set up a global rollback, or if a local rollback is required due to an exception, then it implements a rollback-related strategy; if no rollback is required, it determines whether the transaction is a savepoint transaction. If so, release the current savepoint, if not, determine whether the current transaction is the outermost transaction, and if so, commit action will take place. In addition, before completion and after completion events are triggered before and after transaction submission, respectively. The following is the source code for the commitTransaction AfterReturning () method:

protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        if (logger.isTraceEnabled()) {
            logger.trace("Completing transaction for [" 
                         + txInfo.getJoinpointIdentification() + "]");
        }
        // If the transaction information and state are not empty, the transaction commit policy is executed
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
}
@Override
public final void commit(TransactionStatus status) throws TransactionException {
    // If the current transaction is not completed, an exception is thrown
    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 the current transaction is set to require local rollback due to exceptions, rollback occurs
    if (defStatus.isLocalRollbackOnly()) {
        if (defStatus.isDebug()) {
            logger.debug("Transactional code has requested rollback");
        }
        processRollback(defStatus, false);
        return;
    }

    // If the current transaction sets up a global transaction that needs to be rolled back, the transaction rollback is performed
    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;
    }

    // Execute Transaction Submission Order
    processCommit(defStatus);
}

private void processCommit(DefaultTransactionStatus status) throws TransactionException {
    try {
        // Mark the completion event before the master triggers completion
        boolean beforeCompletionInvoked = false;

        try {
            boolean unexpectedRollback = false;
            // Prepare transaction submission related operations, here is just a hook method for providing custom transactions to subclasses
            prepareForCommit(status);
            // Triggering the before commit event
            triggerBeforeCommit(status);
            // Trigger the before completion event
            triggerBeforeCompletion(status);
            // Identify whether the before completion event has been invoked
            beforeCompletionInvoked = true;

            // If the current transaction is a savepoint transaction, the current savepoint is released to allow the outer transaction to continue executing
            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Releasing transaction savepoint");
                }
                unexpectedRollback = status.isGlobalRollbackOnly();
                status.releaseHeldSavepoint();
            } else if (status.isNewTransaction()) {
                if (status.isDebug()) {
                    logger.debug("Initiating transaction commit");
                }
                // If the current transaction is an outermost transaction, the transaction is committed
                unexpectedRollback = status.isGlobalRollbackOnly();
                doCommit(status);
            } else if (isFailEarlyOnGlobalRollbackOnly()) {
                // If the global rollback needs to be identified, then rollback
                unexpectedRollback = status.isGlobalRollbackOnly();
            }

            // Roll back by throwing an exception
            if (unexpectedRollback) {
                throw new UnexpectedRollbackException(
                    "Transaction silently rolled back because it has been"  
                    + " marked as rollback-only");
            }
        } catch (UnexpectedRollbackException ex) {
            // Triggering the after completion event when an exception is thrown
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
            throw ex;
        } catch (TransactionException ex) {
            // Determine if you need to roll back when commit fails, rollback, or trigger the after completion event
            if (isRollbackOnCommitFailure()) {
                doRollbackOnCommitException(status, ex);
            } else {
                triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
            }
            throw ex;
        } catch (RuntimeException | Error ex) {
            // If the before completion event is not triggered, it is triggered again
            if (!beforeCompletionInvoked) {
                triggerBeforeCompletion(status);
            }
            doRollbackOnCommitException(status, ex);
            throw ex;
        }
        
        try {
            // Triggering the after commit event
            triggerAfterCommit(status);
        } finally {
            // Triggering the after completion event
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
        }

    } finally {
        // Clean up the transaction status information saved in the current thread
        cleanupAfterCompletion(status);
    }
}

6. summary

This paper first explains the overall structure of Spring transactions, then explains how Spring handles transaction propaganda, and then introduces in detail the principles of transaction creation, rollback and submission.

Posted by LucienFB on Sat, 11 May 2019 17:02:18 -0700