References:

1. Deep analysis of Spring transaction principle (highly recommended)

conclusion

Spring stores the database connections corresponding to the data source in a ThreadLocal Map, where the key is the data source and the value is the database connection. This has the advantage of ensuring that database connections operating in the same thread are the same and that transactions are used by the business layer without the need to be aware of and manage database connections.

Through the following analysis, we can summarize the process of Spring transaction processing:

  1. Get the @Transactional configuration property values first, including what exceptions occur to roll back, transaction propagation behavior, transaction isolation level, transaction manager, etc
  2. Gets the transaction manager, or from the Spring container if not configured in @Transactional
  3. Collect transaction information. In this step, different processing is performed according to transaction propagation behavior, such as suspending parent transaction and starting child transaction if it is REQUIRES_new, joining current transaction if it is required, starting new transaction if the current transaction is not started, etc
  4. Implement target method
  5. Check whether this exception is the same as the @Transactional rollbackFor exception or a subclass of it. If so, rollback; otherwise commit the transaction.
  6. Clean up the transaction environment and remove ThreadLocal variables as if the transaction had not been started
  7. No exception occurs and the transaction is committed or rolled back. If the rollback flag bit is set for the current transaction, the transaction is rolled back (to savepoint, the entire transaction is rolled back). If the current transaction is a new transaction, the transaction is committed

Propagation behavior of transactions

Required (often used) If the current thread is already in a transaction, join the transaction. If there is no transaction, create a new one
Requires_new (common) Create a new transaction and suspend the current transaction regardless of whether one exists
support Joins if the current thread has a transaction. Otherwise, transactions are not used
not_supported If the current thread has a transaction, the transaction is suspended. Transactions are not supported
mandatory Joins if the current thread has a transaction. Otherwise throw an exception
never Transactions are not supported and the current thread transaction throws an exception
nested Nested transactions, similar to required but different. Mysql is implemented with savepoints

The main process

The flow code for Spring transactions is mainly in TransactionAspectSupport#invokeWithinTransaction

protected Object invokeWithinTransaction(Method method, @NullableClass<? > targetClass,final InvocationCallback invocation) throws Throwable {

		// If the transaction attribute is null, the method is non-transactional.
		TransactionAttributeSource tas = getTransactionAttributeSource();
    
		// 1. Get the @transactional attribute value
		finalTransactionAttribute txAttr = (tas ! =null ? tas.getTransactionAttribute(method, targetClass) : null);
		// 2. Get the transaction manager
		final PlatformTransactionManager tm = determineTransactionManager(txAttr);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null| |! (tminstanceof CallbackPreferringPlatformTransactionManager)) {
			// Standard transaction demarcation with getTransaction and commit/rollback calls.
			// 3. Collect transaction information and enable or join a transaction based on the configured transaction propagation feature
			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.
				// 4. Execute the intercepted method
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// target invocation exception
				// 5. Decide whether to rollback or commit the transaction according to the exception specified by rollbackFor in the @Transactional configuration.
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				// 6. Clear variables set by ThreadLocal as if transactions were not started
				cleanupTransactionInfo(txInfo);
			}
			// 7. Commit or rollback the transaction
			commitTransactionAfterReturning(txInfo);
			return retVal;
		}

		else {
            // Process of programmatic transaction (omitted)
        }
Copy the code

Get transaction attributes

Transactional has the following attributes:

Fields of primary concern:

  1. RollbackFor: rollback is performed when xx exceptions occur. The default values are RuntimeException and Error
  2. Propagation: Propagation behavior (default required, requiresNew, support, etc.)
  3. Isolation: Isolation level (read uncommitted, read Committed, repeatable read, serialization)

Get the transaction manager

TransactionManager: the TransactionManager, which holds the current data source connection and provides actions related to database transactions, including starting, suspending, and rolling back transactions. A data source requires a transaction manager.

Transaction manager acquisition process:

  1. Get it first from the @Transactional attribute configuration;
  2. If the annotation is not specified the transaction manager, from the spring container to find an instance object implementing the PlatformTransactionManager interface. An error is reported if there are multiple instances of this interface that do not use the @primary annotation

Collecting transaction information (critical)

The code is: entrance TransactionAspectSupport# createTransactionIfNecessary

protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
			@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
		// Omit some code

		TransactionStatus status = null;
		if(txAttr ! =null) {
			if(tm ! =null) {
				// Get transaction status
				status = tm.getTransaction(txAttr);
			}
			// Omit some code
		}
		// Prepare a transaction message (TransactionInfo)
		return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
	}
Copy the code

Tm.gettransaction (txAttr) method is analyzed and processed as follows:

  1. The transaction object (DataSourceTransactionObject)
  2. Determines whether the current thread has started a transaction and, if so, processes it according to propagation behavior
  3. If the transaction is not started, the transaction state is built and a new transaction is started

The detailed code is as follows:

@Override
	public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
		// 1. Get the transaction object
        Object transaction = doGetTransaction();

		/ /... Omit some code
        
		// 2. Check whether the current thread has started the transaction
		if (isExistingTransaction(transaction)) {
			// Existing transaction found -> check propagation behavior to find out how to behave.
			// 3. If a transaction exists, perform different operations according to the propagation behavior
            return handleExistingTransaction(definition, transaction, debugEnabled);
		}

		/ /... Omit some code
		
        // 4. The current thread does not start the transaction for the first time, execute the following code, according to the propagation behavior
		// No existing transaction found -> check propagation behavior to find out how to proceed.

        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
            // The propagation behavior is mandatory, so an exception is thrown because the transaction is not started
			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) {
			SuspendedResourcesHolder suspendedResources = suspend(null);
			if (debugEnabled) {
				logger.debug("Creating new transaction with name [" + definition.getName() + "]." + definition);
			}
			try {
				booleannewSynchronization = (getTransactionSynchronization() ! = SYNCHRONIZATION_NEVER);// Build the transaction state
				DefaultTransactionStatus status = newTransactionStatus(
						definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
				// Start transaction
                doBegin(transaction, definition);
				prepareSynchronization(status, definition);
				return status;
			}
			catch (RuntimeException | Error ex) {
				resume(null, suspendedResources);
				throwex; }}/ /... Omit some code
	}
Copy the code

Get transaction object

org.springframework.jdbc.datasource.DataSourceTransactionManager#doGetTransaction

@Override
protected Object doGetTransaction(a) {
    DataSourceTransactionObject txObject = new DataSourceTransactionObject();
    txObject.setSavepointAllowed(isNestedTransactionAllowed());
    / / from TransactionSynchronizationManager based on the current data source access to a database connection object ConnectionHolder
    ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
    txObject.setConnectionHolder(conHolder, false);
    return txObject;
}
Copy the code

TransactionSynchronizationManager is the affairs of each thread synchronization manager, used to manage affairs related resources. In this class, we can see many ThreadLocal variables

DataSource = DataSource; // DataSource = DataSource; // DataSource = DataSource; // DataSource = DataSource

private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");

private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
    new NamedThreadLocal<>("Transaction synchronizations");

private static final ThreadLocal<String> currentTransactionName =
    new NamedThreadLocal<>("Current transaction name");

private static final ThreadLocal<Boolean> currentTransactionReadOnly =
    new NamedThreadLocal<>("Current transaction read-only status");

private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
    new NamedThreadLocal<>("Current transaction isolation level");

private static final ThreadLocal<Boolean> actualTransactionActive =
    new NamedThreadLocal<>("Actual transaction active");
Copy the code

Checks whether the current thread has started a transaction

There are two rules for judging:

  1. Whether the transaction object of the current thread holds a ConnectionHolder
  2. ConnectionHolder whether the transaction is active

Detailed code is as follows: DataSourceTransactionManager# isExistingTransaction

@Override
protected boolean isExistingTransaction(Object transaction) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive());
}
Copy the code

If the current thread does not have a transaction, start a new transaction

To start a transaction, perform the following steps:

  1. Build TransactionStatus
  2. Perform the doBegin operation
  3. Set the properties related to the current transaction to ThreadLocal, such as isolation level, propagation behavior

Let’s start with TransactionStatus, which is needed when a transaction manager commits or rolls back a transaction. The main contents of the records include:

  • Whether the current transaction is a new transaction or joins someone else’s transaction. (Required and REQUIRES_new required)
  • Is there a save point
  • Whether the rollback flag bit has been set. (Setting the rollback flag bit does not actually perform a rollback, but determines when and if a rollback is actually performed based on propagation behavior)
  • Whether the transaction has completed

After the transaction state is obtained, the doBegin operation does four things:

  1. Gets a database connection based on the data source object
  2. Setting the Isolation Level
  3. performcon.setAutoCommit(false), start the transaction
  4. Bind this database connection CON to the current thread. DataSource as key, con as value, saves the key-value pair to a Map of ThreadLocal

Finally, encapsulate the transaction state, transaction manager, and transaction properties into a TransactionInfo object.

If the current thread already has a transaction

The focus is on the difference between the common requires_new and required

/** * Create a TransactionStatus for an existing transaction. */
private TransactionStatus handleExistingTransaction(
    TransactionDefinition definition, Object transaction, boolean debugEnabled)
    throws TransactionException {

    / /... Omit other transmission actions

    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
        // Suspend the transaction
        SuspendedResourcesHolder suspendedResources = suspend(transaction);
        try {
            booleannewSynchronization = (getTransactionSynchronization() ! = SYNCHRONIZATION_NEVER);// Familiar code, start a new transaction
            DefaultTransactionStatus status = newTransactionStatus(
                definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
            doBegin(transaction, definition);
            prepareSynchronization(status, definition);
            return status;
        }
        catch (RuntimeException | Error beginEx) {
            resumeAfterBeginException(transaction, suspendedResources, beginEx);
            throwbeginEx; }}// The propagation behavior is support or required
    // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED.
    / /... Omit some code
    return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
}
Copy the code

As you can see from the code above, when the isolation level is REQUIRES_new, a transaction is suspended and then a new transaction is started. When the isolation level is Support or Required, the response is returned without suspend or doBegin.

Note: When the propagation behavior is required, the isolation level does not change and remains the isolation level of the previous transaction.

Exception handling

When the transaction information is collected, the target method is executed. If an exception occurs during execution, the transaction is rolled back or committed based on the exception specified by the @Transactional configuration rollbackFor.

/**
  * Handle a throwable, completing the transaction.
  * We may commit or roll back, depending on the configuration.
  * @param txInfo information about the current transaction
  * @param ex throwable encountered
  */
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
    if(txInfo ! =null&& txInfo.getTransactionStatus() ! =null) {
        / /... Omit some code
        
        // If the exception thrown is the specified rollback exception, or the default value, the rollback is performed. Otherwise, commit the transaction
        if(txInfo.transactionAttribute ! =null && txInfo.transactionAttribute.rollbackOn(ex)) {
            try {
                txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
            }
            / /... Omit some code
        }
        else {
            // We don't roll back on this exception.
            // Will still roll back if TransactionStatus.isRollbackOnly() is true.
            try {
                txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
            }
            / /... Omit some code}}}Copy the code

Commit the transaction

If no exception occurs, the transaction is rolled back or committed according to whether the rollback flag bit is set.

/**
  * Execute after successful completion of call, but not after an exception was handled.
  * Do nothing if we didn't create a transaction.
  * @param txInfo information about the current transaction
  */
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
    if(txInfo ! =null&& txInfo.getTransactionStatus() ! =null) {
        if (logger.isTraceEnabled()) {
            logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]"); } txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); }}/** * This implementation of commit handles participating in existing * transactions and programmatic rollback requests.  * Delegates to {@code isRollbackOnly}, {@code doCommit}
  * and {@code rollback}.
  * @see org.springframework.transaction.TransactionStatus#isRollbackOnly()
  * @see #doCommit
  * @see #rollback
  */
@Override
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 the rollback flag bit is set, the rollback is performed
    if (defStatus.isLocalRollbackOnly()) {
        if (defStatus.isDebug()) {
            logger.debug("Transactional code has requested rollback");
        }
        processRollback(defStatus, false);
        return;
    }
	/ /... Omit some code
    // Commit the transaction
    processCommit(defStatus);
}
Copy the code

processCommit

Where the commit is actually performed, you need to determine whether the current transaction is a new transaction and, if so, commit.

/**
  * Process an actual commit.
  * Rollback-only flags have already been checked and applied.
  * @param status object representing the transaction
  * @throws TransactionException in case of commit failure
  */
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
    try {
        boolean beforeCompletionInvoked = false;

        try {
            boolean unexpectedRollback = false;
            prepareForCommit(status);
            triggerBeforeCommit(status);
            triggerBeforeCompletion(status);
            beforeCompletionInvoked = true;
			// Save point exists, release save point
            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Releasing transaction savepoint");
                }
                unexpectedRollback = status.isGlobalRollbackOnly();
                status.releaseHeldSavepoint();
            }
            // The new transaction is committed
            else if (status.isNewTransaction()) {
                if (status.isDebug()) {
                    logger.debug("Initiating transaction commit");
                }
                unexpectedRollback = status.isGlobalRollbackOnly();
                doCommit(status);
            }
            else if (isFailEarlyOnGlobalRollbackOnly()) {
                unexpectedRollback = status.isGlobalRollbackOnly();
            }

            // Throw UnexpectedRollbackException if we have a global rollback-only
            // marker but still didn't get a corresponding exception from commit.
            if (unexpectedRollback) {
                throw new UnexpectedRollbackException(
                    "Transaction silently rolled back because it has been marked as rollback-only"); }}/ /... Omit some code
    }
    finally{ cleanupAfterCompletion(status); }}Copy the code

processRollback

The rollback process is to roll back to a savepoint if there is one. If it is a new transaction, the rollback operation is performed directly. If you join another transaction, the rollback flag is set instead of being performed.

/**
  * Process an actual rollback.
  * The completed flag has already been checked.
  * @param status object representing the transaction
  * @throws TransactionException in case of rollback failure
  */
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
    try {
        boolean unexpectedRollback = unexpected;

        try {
            triggerBeforeCompletion(status);
			// There are savepoints, rollback to savepoints
            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Rolling back transaction to savepoint");
                }
                status.rollbackToHeldSavepoint();
            }
            // New transaction, rollback directly
            else if (status.isNewTransaction()) {
                if (status.isDebug()) {
                    logger.debug("Initiating transaction rollback");
                }
                doRollback(status);
            }
            else {
                // Join the other transaction and set the rollback flag bit
                // Participating in larger transaction
                if (status.hasTransaction()) {
                    if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                        if (status.isDebug()) {
                            logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                        }
                        doSetRollbackOnly(status);
                    }
                  
                / /... Omit some code
    finally{ cleanupAfterCompletion(status); }}Copy the code