[Spring] Execution principle of transaction

rollback of transaction

  1. If the acquired transaction attribute is not empty and the thrown exception is of type RuntimeException or Error, call the rollback method in the transaction manager to roll back

  2. If the transaction property is empty or the exception thrown is not RuntimeException or Error, the transaction will continue to be committed

public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {

    /**
     * Handle transactions that throw exceptions
     */
    protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
        // Empty
        if (txInfo != null && txInfo.getTransactionStatus() != null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
                        "] after exception: " + ex);
            }
            // If the transaction property is not empty and the exception is a RuntimeException or Error
            if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
                try {
                    // Get the transaction manager and call the rollback method to roll back
                    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 {
                // If the transaction attribute is empty or the exception is not RuntimeException or Error, continue to submit the transaction
                try {
                    // submit
                    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;
                }
            }
        }
    }
}

// The rollbackOn method is implemented in DefaultTransactionAttribute
public class DefaultTransactionAttribute extends DefaultTransactionDefinition implements TransactionAttribute {
    /**
     * Determine whether it is RuntimeException or Error
     */
    @Override
    public boolean rollbackOn(Throwable ex) {
        return (ex instanceof RuntimeException || ex instanceof Error);
    }
}

The rollback method is implemented in AbstractPlatformTransactionManager and is mainly divided into the following three situations:

  1. Determine whether the transaction has set a savepoint, and if set, roll back the transaction to the savepoint
  2. If it is an independent new transaction, it can be rolled back directly
  3. If neither a savepoint nor a new transaction is set, it means that it may be in a nested transaction. At this time, only the rollback status rollbackOnly is set to true. When its peripheral transaction is committed, if the rollback status is found to be true, then do not submit

After the above steps are executed, call the cleanupAfterCompletion method to clean up the resources and restore the suspended transaction.

public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable {
    /*
     * rollback
     */
    @Override
    public final void rollback(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");
        }
        // Go to DefaultTransactionStatus
        DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
        // handle rollback
        processRollback(defStatus, false);
    }

    /**
     * handle rollback
     */
    private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
        try {
            boolean unexpectedRollback = unexpected;

            try {
                // Rollback the previous trigger
                triggerBeforeCompletion(status);
                // Is there a savepoint
                if (status.hasSavepoint()) {
                    if (status.isDebug()) {
                        logger.debug("Rolling back transaction to savepoint");
                    }
                    // rollback to savepoint
                    status.rollbackToHeldSavepoint();
                }
                else if (status.isNewTransaction()) { // If it is an independent new transaction
                    if (status.isDebug()) {
                        logger.debug("Initiating transaction rollback");
                    }
                    // rollback directly
                    doRollback(status);
                }
                else {
                    // If in a nested transaction summary
                    if (status.hasTransaction()) {
                        // If the local rollback status is set to true or the transaction fails, perform a global rollback
                        if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                            if (status.isDebug()) {
                                logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                            }
                            // Set transaction rollbackOnly status to true
                            doSetRollbackOnly(stat);
                        }
                        else {
                            // Print the log, meaning it is up to the organizer of the transaction to decide whether to roll back
                            if (status.isDebug()) {
                                logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
                            }
                        }
                    }
                    else {
                        // Print DEBUG log, it should be rolled back but no transaction is obtained
                        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;
            }
            // trigger after rollback
            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 {
            // Clear related resources and resume pending transactions
            cleanupAfterCompletion(status);
        }
    }

    /**
     * When a transaction fails, whether to roll back the transaction globally
     */
    public final boolean isGlobalRollbackOnParticipationFailure() {
        return this.globalRollbackOnParticipationFailure;
    }
}

rollback processing

rollbackToHeldSavepoint rollback to savepoint

The rollbackToHeldSavepoint method is implemented in AbstractTransactionStatus, which calls the getSavepointManager method to obtain the savepoint manager, and calls the rollbackToSavepoint method of the SavepointManager to roll back:

public abstract class AbstractTransactionStatus implements TransactionStatus {

	public void rollbackToHeldSavepoint() throws TransactionException {
		Object savepoint = getSavepoint();
		if (savepoint == null) {
			throw new TransactionUsageException(
					"Cannot roll back to savepoint - no savepoint associated with current transaction");
		}
		// The getSavepointManager method is implemented in DefaultTransactionStatus
		getSavepointManager().rollbackToSavepoint(savepoint);
		getSavepointManager().releaseSavepoint(savepoint);
		setSavepoint(null);
	}
}

SavepointManager is an interface, and its inheritance relationship is as follows:

The method of getting SavepointManager in DefaultTransactionStatus:

  1. Get the transaction object, the previous knowledge shows that this is a DataSourceTransactionObject
  2. It can be seen from the inheritance relationship that DataSourceTransactionObject is also a subclass of SavepointManager, so convert DataSourceTransactionObject to SavepointManager and return it
public class DefaultTransactionStatus extends AbstractTransactionStatus {
	@Override
	protected SavepointManager getSavepointManager() {
		// The previous knowledge shows that here is a DataSourceTransactionObject
		Object transaction = this.transaction;
		if (!(transaction instanceof SavepointManager)) {
			throw new NestedTransactionNotSupportedException(
					"Transaction object [" + this.transaction + "] does not support savepoints");
		}
		// Convert DataSourceTransactionObject to SavepointManager
		return (SavepointManager) transaction;
	}
}

DataSourceTransactionObject is an inner class of DataSourceTransactionManager, which inherits JdbcTransactionObjectSupport, and the rollbackToSavepoint method is implemented in JdbcTransactionObjectSupport:

  1. Get ConnectionHolder, ConnectionHolder holds database connection
  2. Call the underlying rollback method to roll back the transaction to the savepoint
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
        implements ResourceTransactionManager, InitializingBean {
    // Inner class DataSourceTransactionObject
    private static class DataSourceTransactionObject extends JdbcTransactionObjectSupport {

    }
}

// JdbcTransactionObjectSupport
public abstract class JdbcTransactionObjectSupport implements SavepointManager, SmartTransactionObject {
    /**
     * rollback to savepoint
     */
    @Override
    public void rollbackToSavepoint(Object savepoint) throws TransactionException {
        // GetConnectionHolder
        ConnectionHolder conHolder = getConnectionHolderForSavepoint();
        try {
            // Call the underlying rollback method to roll back the transaction to the savepoint
            conHolder.getConnection().rollback((Savepoint) savepoint);
            conHolder.resetRollbackOnly();
        }
        catch (Throwable ex) {
            throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex);
        }
    }

    /**
     * release savepoint
     */
    @Override
    public void releaseSavepoint(Object savepoint) throws TransactionException {
        ConnectionHolder conHolder = getConnectionHolderForSavepoint();
        try {
            // Call the underlying method to release the savepoint
            conHolder.getConnection().releaseSavepoint((Savepoint) savepoint);
        }
        catch (Throwable ex) {
            logger.debug("Could not explicitly release JDBC savepoint", ex);
        }
    }
}

doRollback transaction rollback

When rolling back a transaction, first obtain a database connection, and then call the underlying rollback to roll back:

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
		implements ResourceTransactionManager, InitializingBean {

	@Override
	protected void doRollback(DefaultTransactionStatus status) {
		// Get the data source transaction object
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
		// get database connection
		Connection con = txObject.getConnectionHolder().getConnection();
		if (status.isDebug()) {
			logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
		}
		try {
			// Call the underlying rollback method
			con.rollback();
		}
		catch (SQLException ex) {
			throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
		}
	}
}

doSetRollbackOnly sets the rollback state

The doSetRollbackOnly method is implemented in DataSourceTransactionManager:

  1. Convert the transaction to a DataSourceTransactionObject object. As mentioned earlier, the DataSourceTransactionObject holds the database connection object ConnectionHolder

  2. Set the rollbackOnly property of ConnectionHolder to true, first mark the rollback status of the transaction, and hand it over to the external transaction for judgment and rollback.

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
        implements ResourceTransactionManager, InitializingBean {

    @Override
    protected void doSetRollbackOnly(DefaultTransactionStatus status) {
        // Get the data source transaction object
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
        if (status.isDebug()) {
            logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() +
                    "] rollback-only");
        }
        // set rollback state
        txObject.setRollbackOnly();
    }

    /**
     * Inner class DataSourceTransactionObject
     */
    private static class DataSourceTransactionObject extends JdbcTransactionObjectSupport {

        // omit other methods

        // set rollback state
        public void setRollbackOnly() {
            // Set the rollbackOnly property of ConnectionHolder to true and implement it in the parent class ResourceHolderSupport of ConnectionHolder
            getConnectionHolder().setRollbackOnly();
        }

    }

}

// ConnectionHolder's parent class ResourceHolderSupport
public abstract class ResourceHolderSupport implements ResourceHolder {

    private boolean rollbackOnly = false;

    /**
     * Mark the transaction rollback status as true
     */
    public void setRollbackOnly() {
        this.rollbackOnly = true;
    }

}

Resource cleanup

After the transaction is rolled back, it is necessary to clean up related resources and resume the suspended transaction:

  1. If the newSynchronization status of the transaction is true, clear the transaction-related information bound to the current thread
    • Implemented in the clear method of TransactionSynchronizationManager to clear the transaction name, transaction isolation level and other information bound to the current thread
  2. If it is a new transaction, clear the binding relationship between the current thread and the database connection, which is implemented in the doCleanupAfterCompletion method of DataSourceTransactionManager
  3. If the pending transaction is not empty, resume the pending transaction
    • Get the data source and restore the binding relationship between the data source and the suspended transaction
    • Recovers synchronization information of a suspended transaction with the current thread
public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable {

        /**
	 * Cleanup after rollback
	 * @see #doCleanupAfterCompletion
	 */
	private void cleanupAfterCompletion(DefaultTransactionStatus status) {
		status.setCompleted();
		if (status.isNewSynchronization()) {
			// Clear the information bound by the current thread
			TransactionSynchronizationManager.clear();
		}
		// If it is a new transaction
		if (status.isNewTransaction()) {
			// Clear the binding relationship between the current thread and the database connection
			doCleanupAfterCompletion(status.getTransaction());
		}
		// If the pending transaction is not empty
		if (status.getSuspendedResources() != null) {
			if (status.isDebug()) {
				logger.debug("Resuming suspended transaction after completion of inner transaction");
			}
			Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
			// Resume a pending transaction
			resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
		}
	}

       /**
	 * Resume a pending transaction
	 * @see #doResume
	 * @see #suspend
	 */
	protected final void resume(@Nullable Object transaction, @Nullable SuspendedResourcesHolder resourcesHolder)
			throws TransactionException {

		if (resourcesHolder != null) {
			// Get pending transactions
			Object suspendedResources = resourcesHolder.suspendedResources;
			if (suspendedResources != null) {
				// Get the data source and bind with the pending transaction
				doResume(transaction, suspendedResources);
			}
			// Synchronization information for pending transactions
			List<TransactionSynchronization> suspendedSynchronizations = resourcesHolder.suspendedSynchronizations;
			if (suspendedSynchronizations != null) {
				// Restore transaction and thread synchronization information
				TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive);
				TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel);
				TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly);
				TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name);
				doResumeSynchronization(suspendedSynchronizations);
			}
		}
	}
}

// DataSourceTransactionManager
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
		implements ResourceTransactionManager, InitializingBean {
	// Mainly to clear the binding relationship between the current thread and the database connection
	protected void doCleanupAfterCompletion(Object transaction) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;

		// Remove the connection holder from the thread, if exposed.
		if (txObject.isNewConnectionHolder()) {
			TransactionSynchronizationManager.unbindResource(obtainDataSource());
		}

		// get database connection
		Connection con = txObject.getConnectionHolder().getConnection();
		try {
			if (txObject.isMustRestoreAutoCommit()) {
				// autocommit set to true
				con.setAutoCommit(true);
			}
			// Reset database connection related settings
			DataSourceUtils.resetConnectionAfterTransaction(
					con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly());
		}
		catch (Throwable ex) {
			logger.debug("Could not reset JDBC Connection after transaction", ex);
		}

		if (txObject.isNewConnectionHolder()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Releasing JDBC Connection [" + con + "] after transaction");
			}
			// release the connection
			DataSourceUtils.releaseConnection(con, this.dataSource);
		}
		// Clear the binding relationship between the current thread and the database connection
		txObject.getConnectionHolder().clear();
	}

	@Override
	protected void doResume(@Nullable Object transaction, Object suspendedResources) {
		// Get the data source and bind with the pending transaction
		TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources);
	}
}

// TransactionSynchronizationManager
public abstract class TransactionSynchronizationManager {

    // The database resource information bound by the thread is saved. The Key in the Map is the KEY constructed by the data source, and the value is the corresponding ConnectionHolder.
    private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");
    // Save the thread-bound transaction synchronization information TransactionSynchronization
    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
            new NamedThreadLocal<>("Transaction synchronizations");

    // The transaction name that holds the thread binding
    private static final ThreadLocal<String> currentTransactionName =
            new NamedThreadLocal<>("Current transaction name");
    // Saves the read-only state of a thread-bound transaction
    private static final ThreadLocal<Boolean> currentTransactionReadOnly =
            new NamedThreadLocal<>("Current transaction read-only status");

    // Saves the thread-bound transaction isolation level
    private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
            new NamedThreadLocal<>("Current transaction isolation level");
    // Saves the active state of the thread-bound transaction
    private static final ThreadLocal<Boolean> actualTransactionActive =
            new NamedThreadLocal<>("Actual transaction active");

    /**
     * Clean up the various synchronization states of the transaction with the current thread
     */
    public static void clear() {
        // Clear the transaction synchronization information bound by the current thread TransactionSynchronization
        synchronizations.remove();
        // Clears the transaction name bound to the current thread
        currentTransactionName.remove();
        // Clears the read-only state of a thread-bound transaction
        currentTransactionReadOnly.remove();
        // Clear the thread-bound transaction isolation level
        currentTransactionIsolationLevel.remove();
        // Clear thread-bound transaction active state
        actualTransactionActive.remove();
    }

}

Summarize

refer to

[Cat kiss fish] Spring source code analysis: complete collection

Spring version: 5.2.5.RELEASE

Tags: Java Spring Back-end

Posted by (RL)Ian on Fri, 06 May 2022 07:27:05 +0300