Shallow introduction to the implementation principle of spring transaction propagation

Time:2021-10-26

This article analyzes the source code related to spring transactions with you. It is long and has many code fragments. It is recommended to use a computer to read it

Objective of this paper

  • Understand the spring transaction management core interface
  • Understand the core logic of spring transaction management
  • Understand the propagation type of transaction and its implementation principle

edition

SpringBoot 2.3.3.RELEASE

What is transaction propagation?

Spring not only encapsulates transaction control, but also AbstractsPropagation of transactionsIn this concept, transaction propagation is not defined by relational database, but an enhanced extension made by spring when encapsulating transactions@TransactionalSpecify the propagation of the transaction. The specific types are as follows

Transaction propagation behavior type explain
PROPAGATION_REQUIRED If there is no current transaction, create a new transaction. If there is already a transaction, join it.Spring’s default transaction propagation type
PROPAGATION_SUPPORTS The current transaction is supported. If there is no current transaction, it will be executed in a non transactional manner.
PROPAGATION_MANDATORY Use the current transaction. If there is no current transaction, throw an exception.
PROPAGATION_REQUIRES_NEW Create a new transaction. If there is a current transaction, suspend (pause) the current transaction.
PROPAGATION_NOT_SUPPORTED The operation is performed in a non transactional manner. If there is a current transaction, the current transaction is suspended.
PROPAGATION_NEVER Execute in a non transactional manner. If a transaction currently exists, an exception will be thrown.
PROPAGATION_NESTED If a transaction currently exists, it is executed within a nested transaction. If there is no current transaction, execute the same as the deployment_ Required similar operations.

Take a chestnut

Take nested transactions as an example

@Service
public class DemoServiceImpl implements DemoService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private DemoServiceImpl self;

    @Transactional
    @Override
    public void insertDB() {
        String sql = "INSERT INTO sys_user(`id`, `username`) VALUES (?, ?)";
        jdbcTemplate.update(sql, uuid(), "taven");

        try {
            //The embedded transaction will be rolled back and the external transaction will not be affected
            self.nested();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Transactional(propagation = Propagation.NESTED)
    @Override
    public void nested() {
        String sql = "INSERT INTO sys_user(`id`, `username`) VALUES (?, ?)";
        jdbcTemplate.update(sql, uuid(), "nested");

        throw new RuntimeException("rollback nested");

    }

    private String uuid() {
        return UUID.randomUUID().toString();
    }
}

In the above code, the nested () method marks that the transaction propagation type is nested. Ifnested()Throwing an exception in will only roll backnested()The SQL in the method will not be affectedinsertDB()Method

Note: when a service calls an internal method, if you directly use this call, the transaction will not take effect. Therefore, using this call is equivalent to skipping the external proxy class, so AOP will not take effect and transactions cannot be used

reflection

As we all know, spring transactions are implemented through AOP. What should we do if we write an AOP control transaction ourselves?

//Pseudo code
public Object invokeWithinTransaction() {
    //Open transaction
    connection.beginTransaction();
    try {
        //Reflection execution method
        Object result = invoke();
        //Commit transaction
        connection.commit();
        return result;
    } catch(Exception e) {
        //Rollback on exception
        connection.rollback();
        throw e;
    } 
    
}

On this basis, let’s think about how to realize the propagation of transactions if we do it ourselves

withPROPAGATION_REQUIREDFor example, this seems very simple. Let’s judge whether there is a transaction at present (we can consider using ThreadLocal to store existing transaction objects). If there is a transaction, we won’t open a new transaction. Conversely, without a transaction, we create a new transaction

If the transaction is started by the current aspect, the transaction is committed / rolled back, and vice versa

So how are the current transaction suspended (suspended) and embedded Transactions described in transaction propagation implemented?

<!–

Here’s a spoiler in advance. The embedded transaction is implemented using the savepoint of the relational database
–>

Start with the source code

To read the source code related to transaction propagation, let’s first understand the core interfaces and classes of spring transaction management

  1. TransactionDefinition

This interface defines all the attributes of transactions (isolation level, propagation type, timeout, etc.), which are often used in our daily development@TransactionalIn fact, it will eventually be transformed into transaction definition

  1. TransactionStatus

The status of transactions. Take the most commonly used implementation defaulttransactionstatus as an example. This class stores the current transaction object, savepoint, the currently suspended transaction, whether it is completed, whether it is only rolled back, etc

  1. TransactionManager

This is an empty interface, which directly inherits from the platform transactionmanager (we usually use this, the default implementation class datasourcetransactionmanager) and
Reactive transaction manager (reactive transaction manager, which is not the focus of this article, so we won’t talk more about it)

From the above two interfaces, the main role of transaction manager is

  • Start a transaction through transactiondefinition and return transactionstatus
  • Commit and rollback transactions through transactionstatus (the connection that actually started the transaction is usually stored in transactionstatus)
public interface PlatformTransactionManager extends TransactionManager {
    
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
            throws TransactionException;

    
    void commit(TransactionStatus status) throws TransactionException;

    
    void rollback(TransactionStatus status) throws TransactionException;

}
  1. TransactionInterceptor

Transaction interceptor, the core class of transaction AOP (supporting responsive transactions, programming transactions, and our commonly used standard transactions). Due to space reasons, this paper only discusses the implementation of standard transactions

Let’s start with the transaction interceptor, the entry of transaction logic, to see the core logic of spring transaction management and the implementation of transaction propagation

TransactionInterceptor

Transactioninterceptor implements methodinvocation (which is a way to implement AOP), and its core logic is in the parent classTransactionAspectSupportIn, method location:TransactionInterceptor::invokeWithinTransaction

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();
        //Attribute transactionattribute extensions transactiondefinition of the current transaction
        final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
        //The transaction attribute defines which transaction manager is currently used
        //If there is no definition, go to the spring context to find an available transaction manager
        final TransactionManager tm = determineTransactionManager(txAttr);

        //The processing of responsive transactions is omitted
        
        PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
        final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

        if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
            // Standard transaction demarcation with getTransaction and commit/rollback calls.
            TransactionInfo txInfo = createTransactionIfNecessary(ptm, 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.
                //If there is a next interceptor, it will execute, and eventually it will execute to the target method, that is, our business code
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                // target invocation exception
                //Complete the current transaction (commit or rollback) when an exception is caught
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
                cleanupTransactionInfo(txInfo);
            }

            if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
                // Set rollback-only in case of Vavr failure matching our rollback rules...
                TransactionStatus status = txInfo.getTransactionStatus();
                if (status != null && txAttr != null) {
                    retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                }
            }
            //Commit or rollback according to the status of the transaction
            commitTransactionAfterReturning(txInfo);
            return retVal;
        }

        //Omitting the processing of programmatic transactions
    }

There is a lot of code here. According to the location of comments, we can sort out the core logic

  1. Get the current transaction attributes and the transaction manager (taking annotation transaction as an example), which can be accessed through@TransactionalTo define)
  2. createTransactionIfNecessaryTo determine whether it is necessary to create a transaction
  3. invocation.proceedWithInvocationExecute the interceptor chain and eventually execute to the target method
  4. completeTransactionAfterThrowingWhen an exception is thrown, complete the transaction, commit or rollback, and throw the exception
  5. commitTransactionAfterReturningFrom the perspective of method naming, this method will commit transactions.

However, it will be found in the source code that this method also contains rollback logic, and the specific behavior will be determined according to some states of the current transactionstatus (that is, we can also control transaction rollback by setting the current transactionstatus, not necessarily by throwing exceptions). See detailsAbstractPlatformTransactionManager::commit

<!–

In the business code, you canTransactionAspectSupport.currentTransactionStatus()Get current transactionstatus
–>

Let’s go on and see what createtransaction ifnecessary does

TransactionAspectSupport::createTransactionIfNecessary
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
            @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

        // If no name specified, apply method identification as transaction name.
        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) {
                //Start a transaction through the transaction manager
                status = tm.getTransaction(txAttr);
            }
            else {
                if (logger.isDebugEnabled()) {
                    logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
                            "] because no transaction manager has been configured");
                }
            }
        }
        
        return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
    }

Core logic in createtransaction ifnecessary

  1. Start a transaction through the platform transaction manager
  2. prepareTransactionInfoPrepare transaction information. We’ll talk about what has been done later

Go onPlatformTransactionManager::getTransaction, this method has only one implementationAbstractPlatformTransactionManager::getTransaction

public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
            throws TransactionException {

        // Use defaults if no transaction definition given.
        TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

        //Get the current transaction. This method inherits the subclass of abstractplatformtransactionmanager and implements it by itself
        Object transaction = doGetTransaction();
        boolean debugEnabled = logger.isDebugEnabled();

        //If a transaction currently exists
        if (isExistingTransaction(transaction)) {
            // Existing transaction found -> check propagation behavior to find out how to behave.
            return handleExistingTransaction(def, transaction, debugEnabled);
        }

        // Check definition settings for new transaction.
        if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
            throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
        }

        //Propagation type_ Mandatory, which requires that there must be a transaction at present
        // No existing transaction found -> check propagation behavior to find out how to proceed.
        if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
            throw new IllegalTransactionStateException(
                    "No existing transaction found for transaction marked with propagation 'mandatory'");
        }
        // PROPAGATION_ REQUIRED, PROPAGATION_ REQUIRES_ NEW, PROPAGATION_ Nested creates a transaction when no transaction exists
        else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
                def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
                def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
            SuspendedResourcesHolder suspendedResources = suspend(null);
            if (debugEnabled) {
                logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
            }
            try {
                //Open transaction
                return startTransaction(def, transaction, debugEnabled, suspendedResources);
            }
            catch (RuntimeException | Error ex) {
                resume(null, suspendedResources);
                throw ex;
            }
        }
        else {
            // Create "empty" transaction: no actual transaction, but potentially synchronization.
            if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
                logger.warn("Custom isolation level specified but no actual transaction initiated; " +
                        "isolation level will effectively be ignored: " + def);
            }
            boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
            return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
        }
    }

There is a lot of code, just focus on the comments

  1. doGetTransactionGet current transaction
  2. Called if a transaction existshandleExistingTransactionProcessing, which we will talk about later

Next, it will decide whether to start the transaction according to the propagation of the transaction

  1. If the transaction propagation type isPROPAGATION_MANDATORY, and there is no transaction, an exception is thrown
  2. If the propagation type isPROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, PROPAGATION_NESTED, and no transaction currently exists, callstartTransactionCreate transaction
  3. When 3 and 4 are not met, for examplePROPAGATION_NOT_SUPPORTED, theTransaction synchronization, but no real transaction is created

Spring Transaction synchronizationAs mentioned in a previous blog, portalhttps://www.jianshu.com/p/788…

How does spring manage current transactions

Let’s talk about the abovedoGetTransactionhandleExistingTransaction, these two methods are implemented by different transaction managers

Let’s take springboot’s default transactionmanager, datasourcetransactionmanager, as an example

    @Override
    protected Object doGetTransaction() {
        DataSourceTransactionObject txObject = new DataSourceTransactionObject();
        txObject.setSavepointAllowed(isNestedTransactionAllowed());
        ConnectionHolder conHolder =
                (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
        txObject.setConnectionHolder(conHolder, false);
        return txObject;
    }

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

combinationAbstractPlatformTransactionManager::getTransactionLet’s see,doGetTransactionIn fact, the current connection is obtained.
To judge whether there is a transaction or not is to judge whether the datasourcetransactionobject object contains a connection and whether the connection has started a transaction.

Let’s continueTransactionSynchronizationManager.getResource(obtainDataSource())Gets the logic of the current connection

TransactionSynchronizationManager::getResource
private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");
    
    @Nullable
    // TransactionSynchronizationManager::getResource
    public static Object getResource(Object key) {
        //When the datasourcetransactionmanager calls this method, the data source is used as the key
        
        //Transactionsynchronizationutils:: unwrapresourcefifnecessary if the key is a wrapper class, get the wrapped object
        //We can ignore this logic
        Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
        Object value = doGetResource(actualKey);
        if (value != null && logger.isTraceEnabled()) {
            logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +
                    Thread.currentThread().getName() + "]");
        }
        return value;
    }

    /**
     * Actually check the value of the resource that is bound for the given key.
     */
    @Nullable
    private static Object doGetResource(Object actualKey) {
        Map<Object, Object> map = resources.get();
        if (map == null) {
            return null;
        }
        Object value = map.get(actualKey);
        // Transparently remove ResourceHolder that was marked as void...
        if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
            map.remove(actualKey);
            // Remove entire ThreadLocal if empty...
            if (map.isEmpty()) {
                resources.remove();
            }
            value = null;
        }
        return value;
    }

From here, we can understand how the datasourcetransactionmanager manages the connection between threads. A map is stored in ThreadLocal. The key is the data source object and the value is the connection of the data source in the current thread

The datasourcetransactionmanager will call theTransactionSynchronizationManager::bindResourceBind the connection of the specified data source to the current thread

AbstractPlatformTransactionManager::handleExistingTransaction

Let’s continue to look back. If there are transactions, how to deal with them

private TransactionStatus handleExistingTransaction(
            TransactionDefinition definition, Object transaction, boolean debugEnabled)
            throws TransactionException {

        //Throw an exception if the propagation of a transaction requires non transactional execution
        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
            throw new IllegalTransactionStateException(
                    "Existing transaction found for transaction marked with propagation 'never'");
        }

        // PROPAGATION_ NOT_ Supported if there is a transaction, suspend the current transaction and execute it in a non transactional manner
        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
            if (debugEnabled) {
                logger.debug("Suspending current transaction");
            }
            //Suspend current transaction
            Object suspendedResources = suspend(transaction);
            boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
            //Build a transactionstatus without transaction
            return prepareTransactionStatus(
                    definition, null, false, newSynchronization, debugEnabled, suspendedResources);
        }

        // PROPAGATION_ REQUIRES_ New if there is a transaction, suspend the current transaction and create a new transaction
        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
            if (debugEnabled) {
                logger.debug("Suspending current transaction, creating new transaction with name [" +
                        definition.getName() + "]");
            }
            SuspendedResourcesHolder suspendedResources = suspend(transaction);
            try {
                return startTransaction(definition, transaction, debugEnabled, suspendedResources);
            }
            catch (RuntimeException | Error beginEx) {
                resumeAfterBeginException(transaction, suspendedResources, beginEx);
                throw beginEx;
            }
        }

        // PROPAGATION_ Nested embedded transaction is the example we gave at the beginning
        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
            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() + "]");
            }
            //Non JTA transaction managers are embedded transactions implemented through savepoint
            //Savepoint: a transaction in a relational database can create a restore point and roll back to the restore point
            if (useSavepointForNestedTransaction()) {
                // Create savepoint within existing Spring-managed transaction,
                // through the SavepointManager API implemented by TransactionStatus.
                // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.
                DefaultTransactionStatus status =
                        prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
                //Create restore point
                status.createAndHoldSavepoint();
                return status;
            }
            else {
                // Nested transaction through nested begin and commit/rollback calls.
                // Usually only for JTA: Spring synchronization might get activated here
                // in case of a pre-existing JTA transaction.
                return startTransaction(definition, transaction, debugEnabled, null);
            }
        }

        //If you go to this step, the propagation type must be propagation_ Supports or promotion_ REQUIRED
        // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED.
        if (debugEnabled) {
            logger.debug("Participating in existing transaction");
        }
        
        //Verify whether the transaction definitions in the current method are consistent with the existing transaction definitions
        if (isValidateExistingTransaction()) {
            if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
                Integer currentIsolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
                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()) {
                if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
                    throw new IllegalTransactionStateException("Participating transaction with definition [" +
                            definition + "] is not marked as read-only but existing transaction is");
                }
            }
        }
        boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
        //Build a transactionstatus without starting a transaction
        return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
    }

There is a lot of code here. Just look at the above comments for logic. Here we finally see the long-awaitedPending and embedded transactionsNow, let’s take a look at the implementation of datasourcetransactionmanager

  • Pending transactions: viaTransactionSynchronizationManager::unbindResourceObtain the current connection according to the data source and remove the connection in the resource. The connection is then stored in the transactionstatus object
    // DataSourceTransactionManager::doSuspend
    @Override
    protected Object doSuspend(Object transaction) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        txObject.setConnectionHolder(null);
        return TransactionSynchronizationManager.unbindResource(obtainDataSource());
    }

After the transaction is submitted or rolled back, it is called.AbstractPlatformTransactionManager::cleanupAfterCompletionThe connection cached in transactionstatus will be rebound into resource

  • Embedded transaction: it is implemented through savepoint of relational database. When committing or rolling back, it will be judged that if the current transaction is savepoint, savepoint will be released or rolled back to savepoint. For specific logic referenceAbstractPlatformTransactionManager::processRollbackandAbstractPlatformTransactionManager::processCommit

At this point, the source code analysis of transaction propagation ends

prepareTransactionInfo

The above leaves a question. Let’s take a look at what the preparetransactioninfo method doesTransactionInfoStructure of

    protected static final class TransactionInfo {

        @Nullable
        private final PlatformTransactionManager transactionManager;

        @Nullable
        private final TransactionAttribute transactionAttribute;

        private final String joinpointIdentification;

        @Nullable
        private TransactionStatus transactionStatus;

        @Nullable
        private TransactionInfo oldTransactionInfo;
        
        // ...
    }

The function of this class in spring is to pass objects internally. The latest transactioninfo is stored in ThreadLocal, and its oldtransactioninfo can be found through the current transactioninfo. Each time a transaction is created, a new transactioninfo will be created (whether a real transaction is created or not) and stored in ThreadLocal. After each transaction is completed, the transactioninfo in the current ThreadLocal will be reset to oldtransactioninfo. This structure forms a linked list, so that spring transactions can be logically nested indefinitely

If you feel that you have gained something, you can pay attention to my official account. Your praise and attention are my greatest support.

Recommended Today

Object memory layout and object access location

First, feel the memory layout through an example CustomerTest public class CustomerTest { public static void main(String[] args) { Customer cust = new Customer(); } } Customer image.png Memory layout at this time image.png notice: 1. Runtime metadata:There are some information describing the current instance, such as hash value and lock status.2. Name is the […]