Implementation of propagation characteristics of spring thing source code reading

The Spring transaction propagation feature refers to how transactions are managed between multiple transaction operations. The Spring framework provides different transaction propagation features for defining transaction boundaries and isolation levels. Let’s first review what types of spring transaction propagation there are

Propagation type
  1. PROPAGATION_REQUIRED (default): If a transaction currently exists, join the transaction; if there is no transaction currently, create a new transaction. This is the most common propagation feature, which guarantees that a group of methods are executed in a transaction, and if an exception occurs in one method, all methods will be rolled back.
  2. PROPAGATION_SUPPORTS: Supports the current transaction. If there is no current transaction, it will be executed in a non-transactional manner. This propagation feature is mainly used for read-only operations such as queries. If a transaction currently exists, it is added to the transaction. If there is no transaction, it is executed in a non-transactional manner.
  3. PROPAGATION_MANDATORY: Supports the current transaction, if there is no current transaction, an exception is thrown. This propagation feature requires that a transaction must currently exist, otherwise an exception will be thrown.
  4. PROPAGATION_REQUIRES_NEW: Create a new transaction. If a transaction currently exists, suspend the current transaction. This propagation feature is suitable for scenarios that require independent transactions, which will not be affected even if the external transaction is rolled back.
  5. PROPAGATION_NOT_SUPPORTED: Perform the operation in a non-transactional manner. If a transaction currently exists, the current transaction will be suspended. This propagation feature is suitable for some non-core businesses or operations that do not require transaction support.
  6. PROPAGATION_NEVER: Perform operations in a non-transactional manner, throwing an exception if a transaction currently exists. This propagation feature requires that the method is not allowed to execute within a transaction and will throw an exception if a transaction currently exists.
  7. PROPAGATION_NESTED: If there is currently a transaction, it will be executed within the nested transaction; if there is currently no transaction, a new transaction will be created. This propagation feature is suitable for scenarios where a sub-transaction needs to be created in a parent transaction. The difference from PROPAGATION_REQUIRES_NEW is that nested transactions are executed in a nested manner, that is, the sub-transaction depends on the parent transaction.
Source code analysis

Now that we know the value of the transaction propagation characteristic, let’s take a look at how spring controls transactions. We need to go back to the code logic for obtaining transactions seen in the previous article and take a closer look.

public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
      throws TransactionException {<!-- -->
   //Get the transaction
   Object transaction = doGetTransaction();
   
   if (isExistingTransaction(transaction)) {<!-- -->
   //case2-transaction existence situation
      // Existing transaction found -> check propagation behavior to find out how to behave.
      return handleExistingTransaction(def, transaction, debugEnabled);
   }

   //case1-no transaction
   
   if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {<!-- -->
      throw new IllegalTransactionStateException(
            "No existing transaction found for transaction marked with propagation 'mandatory'");
   }
   else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
         def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
         def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {<!-- -->
         SuspendedResourcesHolder suspendedResources = suspend(null);
         return startTransaction(def, transaction, debugEnabled, suspendedResources);
   }

There are two situations here. The current thread

case1-no transaction

PROPAGATION_MANDATORY throws an exception directly

PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, PROPAGATION_NESTED will open a new transaction. Other types can be executed without transactions, so there is no need to enable transactions.

case2-there is already a transaction

Will enter the handleExistingTransaction method

private TransactionStatus handleExistingTransaction(
      TransactionDefinition definition, Object transaction, boolean debugEnabled)
      throws TransactionException {<!-- -->

   if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {<!-- -->
      throw new IllegalTransactionStateException(
            "Existing transaction found for transaction marked with propagation 'never'");
   }

   if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {<!-- -->
      Object suspendedResources = suspend(transaction);
   }

   if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {<!-- -->
      SuspendedResourcesHolder suspendedResources = suspend(transaction);
      return startTransaction(definition, transaction, debugEnabled, suspendedResources);

   }

   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 (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);
         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);
      }
   }

   // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED.
   if (debugEnabled) {<!-- -->
      logger.debug("Participating in existing transaction");
   }

   return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
}

PROPAGATION_NEVER throws an exception directly, which requires non-transactional operation.

PROPAGATION_NOT_SUPPORTED will suspend the current transaction and run it in non-transaction mode

PROPAGATION_REQUIRES_NEW Suspend the current transaction and create a new transaction

PROPAGATION_NESTED Create subtransaction

PROPAGATION_SUPPORTS, PROPAGATION_REQUIRED directly join the current transaction.

Combination analysis

Constructing a test case makes no sense. Adding a user is a two-step process. The user table and the account table execute two insert statements, and both add @Transactional annotations for transaction control. Adding a user will call the add account.

@Transactional(propagation = Propagation.REQUIRED)
public int insert(User user) {<!-- -->
    String sql = "insert into users(ucode,uname) values(?,?)";
    int i = jdbcTemplate.update(sql,user.getUserNo(),user.getUserName());
    

    Account account = new Account();
    account.setUcode(user.getUserNo());
    account.setBanalce(0d);
   //This method also has @Transactional annotation
    accountService.addAccount(account);
  //int x = i/0;
    return i;
}
1. They are all default values REQUIRED

Submit normally, rollback if exception occurs

2. REQUIRED + PROPAGATION_REQUIRES_NEW

Two transactions will be opened

Exceptions in the internal method will affect the external method. The internal method can be submitted normally without exception. Subsequent exceptions in the external method will roll back the operation of the external method, resulting in partial submission.

This situation is as follows

@Transactional(propagation = Propagation.REQUIRED)
methodA{<!-- -->
@Transactional(propagation = Propagation.REQUIRES_NEW)
methodB();//Normal submission

    //An exception occurs in subsequent code, A rolls back
}

The two transactions here correspond to two database connections.

Because the above logic when encountering REQUIRES_NEW in existing transactions is as follows

handleExistingTransaction(){<!-- -->
     if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {<!-- -->
      SuspendedResourcesHolder suspendedResources = suspend(transaction);
      return startTransaction(definition, transaction, debugEnabled, suspendedResources);
   }
}

There are two steps here. The first is to suspend the current transaction. Specifically, it will be transferred to doSuspend.

protected Object doSuspend(Object transaction) {<!-- -->
   DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
   txObject.setConnectionHolder(null);
   return TransactionSynchronizationManager.unbindResource(obtainDataSource());
}

Clear and unbind the current ConnectionHolder

The second part is to call the startTransaction method to start a new transaction. The doBegin method will be called to start a new transaction. Here, it will be judged based on the connectionHolder whether to obtain a new connection.

if (!txObject.hasConnectionHolder() ||
      txObject.getConnectionHolder().isSynchronizedWithTransaction()) {<!-- -->
   Connection newCon = obtainDataSource().getConnection();
   if (logger.isDebugEnabled()) {<!-- -->
      logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
   }
   txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}

So the second REQUIRES_NEW transaction is a new connection.

So when the second transaction is committed, how is the first transaction recovered?

The sunspend method will return a SuspendedResourcesHolder to save the attribute information of the currently suspended transaction.

SuspendedResourcesHolder has the following properties

 SuspendedResourcesHolder {
   //Storage connectionHolder
   private final Object suspendedResources;
   private List<TransactionSynchronization> suspendedSynchronizations;
   private String name;
   private boolean readOnly;
   private Integer isolationLevel;
   private boolean wasActive;
}

Then it will be stored in TransactionStatus in the startTransaction method.

private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
      boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {<!-- -->

   boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
   
   DefaultTransactionStatus status = newTransactionStatus(
         definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
   doBegin(transaction, definition);
   prepareSynchronization(status, definition);
   return status;
}

Then at the end of each transaction, whether it ends normally (commitTransactionAfterReturning method) or abnormally (completeTransactionAfterThrowing method), the cleanupAfterCompletion() method will be executed.

private void cleanupAfterCompletion(DefaultTransactionStatus status) {<!-- -->
   status.setCompleted();
   if (status.isNewSynchronization()) {<!-- -->
      TransactionSynchronizationManager.clear();
   }
   if (status.isNewTransaction()) {<!-- -->
      doCleanupAfterCompletion(status.getTransaction());
   }
   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(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
   }
}

Here we finally see that if there are SuspendedResources, the original suspended transaction will be resumed.

3. REQUIRED + NESTED

NESTED will open a sub-transaction and set the savepoint, still in the same connection.