preface
How are Spring transactions propagated?
MethodInceptor’s invoke method is called, and the TransactionInterceptor Interceptor is the TransactionInterceptor, which is the implementation class of MethodInceptor. So this article starts directly with this class.
The body of the
The process of invoking transaction facets
public Object invoke(MethodInvocation invocation) throws Throwable { // Work out the target class: may be {@code null}. // The TransactionAttributeSource should be passed the target class // as well as the method, which may be from an interface. Class<? > targetClass = (invocation.getThis() ! = null ? AopUtils.getTargetClass(invocation.getThis()) : null); // Adapt to TransactionAspectSupport's invokeWithinTransaction... return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); }Copy the code
The method itself does nothing but call the parent’s invokeWithinTransaction method. Notice that the last argument is passed in a lambda expression, and the method called in this expression should be familiar. When analyzing the AOP call chain, This method is used to pass to the next section or call the method of the proxied instance.
protected Object invokeWithinTransaction(Method method, @Nullable Class<? > targetClass, final InvocationCallback invocation) throws Throwable { // If the transaction attribute is null, The method is a non - transactional. / / get the transaction attribute class AnnotationTransactionAttributeSource TransactionAttributeSource tas = getTransactionAttributeSource(); Final TransactionAttribute txAttr = (tas! = null ? tas.getTransactionAttribute(method, targetClass) : null); / / get the transaction manager final PlatformTransactionManager tm = determineTransactionManager (txAttr); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || ! (tm instanceof CallbackPreferringPlatformTransactionManager)) { TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null; Try {/ / method calls proceed retVal = invocation. ProceedWithInvocation (); } the catch (Throwable ex) {/ / target invocation exception / / transaction rollback completeTransactionAfterThrowing (txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } / / transaction commit commitTransactionAfterReturning (txInfo); return retVal; } // omit else}Copy the code
This method logic is very clear, the be clear at a glance, if it is on the declarative transaction processing, call createTransactionIfNecessary method open transaction first, and then through the invocation. ProceedWithInvocation calls the next section, If there are no other facets, the method of the proxied class is called, the exception is rolled back, otherwise the transaction is committed, and this is how the Spring transaction facets are executed. However, the main thing we need to understand is how transactions are managed in these methods and how transactions propagate across multiple methods.
The concept of transmissibility of transactions
Because it involves calls between methods, it’s necessary to consider how transactions flow between those methods. So Spring provides seven propagation properties to choose from, which can be viewed as two broad categories: whether or not the current transaction is supported:
- Support the current transaction (within the same transaction) :
- PROPAGATION_REQUIRED: Supports the current transaction, or creates a new one if it does not exist.
- PROPAGATION_MANDATORY: The current transaction is supported, and an exception is thrown if it does not exist.
- PROPAGATION_SUPPORTS: Supports the current transaction, if it does not exist, it is not used.
- Current transaction not supported (not in the same transaction) :
- PROPAGATION_NEVER: Runs in a non-transactional manner, throws an exception if there is a transaction.
- PROPAGATION_NOT_SUPPORTED: Runs in a non-transactional manner, or suspends the current transaction, if one exists.
- PROPAGATION_REQUIRES_NEW: Create a transaction, and suspend the current one if one exists (the two transactions are independent, and the parent rollback does not affect the child transactions).
- PROPAGATION_NESTED: If the current transaction exists, a nested transaction executes (meaning that it must depend on the parent transaction, a child transaction cannot commit independently, and the child transaction must be rolled back if the parent transaction rolls back, or if the parent transaction rolls back, the parent transaction can either roll back or catch exceptions). If there are no transactions currently, an operation similar to PROPAGATION_REQUIRED is performed.
In fact, we mainly use the PROPAGATION_REQUIRED attribute, and some special services may use PROPAGATION_REQUIRES_NEW and PROPAGATION_NESTED. I’ll focus on these three attributes in a hypothetical scenario.
public class A { @Autowired private B b; @Transactional public void addA() { b.addB(); } } public class B { @Transactional public void addB() { // doSomething... }}Copy the code
Above, I created two classes, A and B, each with A transaction method that uses A declarative transaction and uses the default propagation attribute, calling B’s method from A. When addA requests to the call, the first call is a proxy object method, thus will enter createTransactionIfNecessary method open transaction:
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 ! Status = tm.getTransaction(txAttr); Return prepareTransactionInfo(TM, txAttr, joinpointIdentification, Status); prepareTransactionInfo(tm, txAttr, joinpointIdentification, Status); }Copy the code
Actually open transaction through AbstractPlatformTransactionManager did, and this class is an abstract class, specific instantiation objects is that we often configuration DataSourceTransactionManager object in the project.
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { / / key to see here, DataSourceTransactionObject get Object Object transaction = doGetTransaction (); // Cache debug flag to avoid repeated checks. boolean debugEnabled = logger.isDebugEnabled(); if (definition == null) { // Use defaults if no transaction definition given. definition = new DefaultTransactionDefinition(); } // The first time the connectionHolder is empty, If (isExistingTransaction) {// ExistingTransaction found -> Check Propagation behavior to find out how to behave. return handleExistingTransaction(definition, transaction, debugEnabled); } // Check definition settings for new transaction. if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); } // No existing transaction found -> check propagation behavior to find out how to proceed. if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { throw new IllegalTransactionStateException( "No existing transaction found for transaction marked with propagation 'mandatory'"); } / / for the first time in most will go here else if (definition) getPropagationBehavior () = = TransactionDefinition. PROPAGATION_REQUIRED | | definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || Definition. GetPropagationBehavior () = = TransactionDefinition. PROPAGATION_NESTED) {/ / hang SuspendedResourcesHolder first suspendedResources = suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); } try { boolean newSynchronization = (getTransactionSynchronization() ! = SYNCHRONIZATION_NEVER); // Create a transaction state object that encapsulates some information about the transaction object. DefaultTransactionStatus status = newTransactionStatus(Definition, Transaction, true, newSynchronization, debugEnabled, suspendedResources); / / open the transaction, the key see DataSourceTransactionObject doBegin (transaction, definition); PrepareSynchronization (status, definition); return status; } catch (RuntimeException | Error ex) { resume(null, suspendedResources); throw ex; } } else { boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); }}Copy the code
This method rather long process, step by step, first call doGetTransaction method to obtain a DataSourceTransactionObject object, this class is a subclass of JdbcTransactionObjectSupport, The parent class holds a ConnectionHolder object, which, by definition, holds the current connection.
Protected Object doGetTransaction() {// Manage the Connection Object, create a rollback point, roll back according to the rollback point, Release the rollback point DataSourceTransactionObject txObject = new DataSourceTransactionObject (); / / DataSourceTransactionManager txObject. The default is to allow nested affairs setSavepointAllowed (isNestedTransactionAllowed ()); //obtainDataSource() So that's the database connection block object ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource()); txObject.setConnectionHolder(conHolder, false); return txObject; }Copy the code
Trace the getResource method to see that the ConnectionHolder is fetched from ThreadLocal, the current thread, and the key is the DataSource object. But if you think about it, we’re coming in here for the first time, so we’re not going to get anything from here, and if we’re going to get something from here, it’s going to have to be the same thread coming in here the second time or later, when addA calls addB, Also need to pay attention to it when saving ConnectionHolder to DataSourceTransactionObject object is newConnectionHolder attribute is set to false. Will continue in the future, after creating the transaction object, call isExistingTransaction determine whether a transaction already exists, if there is a will call handleExistingTransaction method, this method is the core of transaction propagation, Since we are coming in for the first time, there will be no transaction, so we will skip it. Further down the line, you can see that the different propagation properties are handled, mainly in the following section:
else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || Definition. GetPropagationBehavior () = = TransactionDefinition. PROPAGATION_NESTED) {/ / hang SuspendedResourcesHolder first suspendedResources = suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); } try { boolean newSynchronization = (getTransactionSynchronization() ! = SYNCHRONIZATION_NEVER); // Create a transaction state object that encapsulates some information about the transaction object. DefaultTransactionStatus status = newTransactionStatus(Definition, Transaction, true, newSynchronization, debugEnabled, suspendedResources); / / open the transaction, the key see DataSourceTransactionObject doBegin (transaction, definition); PrepareSynchronization (status, definition); return status; } catch (RuntimeException | Error ex) { resume(null, suspendedResources); throw ex; }}Copy the code
The first time it comes in, PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, and PROPAGATION_NESTED all enter here, and suspend the existing transaction is called first. The DefaultTransactionStatus object is then created using newTransactionStatus, which stores the status of the current transaction. Note that the newTransaction property is set to true, indicating a newTransaction. Once the state object is created, start the transaction with doBegin, which is a template method:
protected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; Connection con = null; Try {// If no database connection exists if (! TxObject. HasConnectionHolder () | | txObject. GetConnectionHolder () isSynchronizedWithTransaction ()) {/ / from the connection pool to obtain inside connection Connection newCon = obtainDataSource().getConnection(); if (logger.isDebugEnabled()) { logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); } / / wrap connection as a ConnectionHolder, then set to the transaction object txObject. SetConnectionHolder (new ConnectionHolder (newCon), true); } txObject.getConnectionHolder().setSynchronizedWithTransaction(true); con = txObject.getConnectionHolder().getConnection(); / / isolation level was obtained from the database connection in the Integer previousIsolationLevel = DataSourceUtils. PrepareConnectionForTransaction (con, definition); txObject.setPreviousIsolationLevel(previousIsolationLevel); // Switch to manual commit if necessary. This is very expensive in some JDBC drivers, // so we don't want to do it unnecessarily (for example if we've explicitly // configured the connection pool to set it already). if (con.getAutoCommit()) { txObject.setMustRestoreAutoCommit(true); if (logger.isDebugEnabled()) { logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); } con.setautoCommit (false); } // Set the read-only transaction from this point in time (point A) to the end of this transaction, other transactions committed data, the transaction will not see! / / set read-only transaction database is to tell, I am not new within the transaction, modify, delete query operation only, do not need database lock operation, such as reducing the pressure of database prepareTransactionalConnection (con, definition); / / submit closed automatically, is already open transaction, the transaction is active txObject getConnectionHolder () setTransactionActive (true); int timeout = determineTimeout(definition); if (timeout ! = TransactionDefinition.TIMEOUT_DEFAULT) { txObject.getConnectionHolder().setTimeoutInSeconds(timeout); } / / Bind the connection holder to the thread. If (txObject. IsNewConnectionHolder ()) {/ / if it is newly created, Is to establish the current thread and database connection relationship TransactionSynchronizationManager bindResource (obtainDataSource (), txObject. GetConnectionHolder ()); } } catch (Throwable ex) { if (txObject.isNewConnectionHolder()) { DataSourceUtils.releaseConnection(con, obtainDataSource()); txObject.setConnectionHolder(null, false); } throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); }}Copy the code
There are six main things that this method does:
- First get connection from the connection pool and saved to the DataSourceTransactionObject object.
- To turn off automatic commit for the database, you turn on transactions.
- Gets the isolation level of the database.
- Sets whether the transaction is read-only based on the property.
- Identify the transaction as an active transaction (transactionActive=true).
- Bind the ConnectionHolder object to the current thread.
Completed by prepareSynchronization transaction attributes and state set to manage TransactionSynchronizationManager objects. Finally returns to the createTransactionIfNecessary method created in TransactionInfo object with the current thread binding and return. The transaction is enabled and the proceedWithInvocation invocation (addA) method is called from class A, and the addB method from class B is called from class B, assuming that no other invocation is available. So will enter into the createTransactionIfNecessary method. But this time it came in through the isExistingTransaction judgment is the existence of the transaction, thus will enter the handleExistingTransaction method:
private TransactionStatus handleExistingTransaction( TransactionDefinition definition, Object transaction, Boolean debugEnabled) throws TransactionException {// No transactions are allowed, Direct exception if (definition. GetPropagationBehavior () = = TransactionDefinition. PROPAGATION_NEVER) {throw new IllegalTransactionStateException( "Existing transaction found for transaction marked with propagation 'never'"); } // Perform operations non-transactionally, if a transaction exists, Suspending the current transaction if (definition. GetPropagationBehavior () = = TransactionDefinition. PROPAGATION_NOT_SUPPORTED) {if (debugEnabled) { logger.debug("Suspending current transaction"); } // suspendedResources = suspend(transaction); boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); // Modify transaction state information, store some transaction information in the current thread, Return prepareTransactionStatus(Definition, NULL, false, newSynchronization, debugEnabled, suspendedResources); } if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { if (debugEnabled) { logger.debug("Suspending current transaction, creating new transaction with name [" + definition.getName() + "]"); } // SuspendedResourcesHolder = suspend(transaction); try { boolean newSynchronization = (getTransactionSynchronization() ! = SYNCHRONIZATION_NEVER); 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); throw beginEx; } } 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() + "]"); } / / the default can be nested transaction the if (useSavepointForNestedTransaction ()) {/ / Create the savepoint within existing Spring - managed transaction, Implemented by TransactionStatus. // Usually uses JDBC 3.0 savepoints.never activates Spring synchronization. DefaultTransactionStatus status = prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); / / create a rollback 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. boolean newSynchronization = (getTransactionSynchronization() ! = SYNCHRONIZATION_NEVER); DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, null); doBegin(transaction, definition); prepareSynchronization(status, definition); return status; }} / / omit Boolean newSynchronization = (getTransactionSynchronization ()! = SYNCHRONIZATION_NEVER); return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); }Copy the code
For each propagation attribute, look at processing the PROPAGATION_REQUIRES_NEW, which requires that a new transaction be opened every time you call it, and therefore suspend the current transaction.
protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) throws TransactionException { if (TransactionSynchronizationManager.isSynchronizationActive()) { List<TransactionSynchronization> suspendedSynchronizations = doSuspendSynchronization(); try { Object suspendedResources = null; // If (transaction! = null) {// Set connectionHolder to empty suspendedResources = doSuspend(transaction); } / / do data reduction operation String name = TransactionSynchronizationManager. GetCurrentTransactionName (); TransactionSynchronizationManager.setCurrentTransactionName(null); boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); TransactionSynchronizationManager.setCurrentTransactionReadOnly(false); Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null); boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive(); TransactionSynchronizationManager.setActualTransactionActive(false); return new SuspendedResourcesHolder( suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive); } catch (RuntimeException | Error ex) { // doSuspend failed - original transaction is still active... doResumeSynchronization(suspendedSynchronizations); throw ex; } } else if (transaction ! = null) { // Transaction active but no synchronization active. Object suspendedResources = doSuspend(transaction); return new SuspendedResourcesHolder(suspendedResources); } else { // Neither transaction nor synchronization active. return null; } } protected Object doSuspend(Object transaction) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; txObject.setConnectionHolder(null); / / remove the binding relationship, return TransactionSynchronizationManager. UnbindResource (obtainDataSource ()); }Copy the code
Here we obviously enter the first if and call the doSuspend method. Overall, suspending the transaction is simple: First set of DataSourceTransactionObject ConnectionHolder is empty and remove with the current thread binding, The unbound ConnectionHolder and other attributes (transaction name, isolation level, read-only attributes) are then wrapped into the SuspendedResourcesHolder object and the active state of the current transaction is set to false. After the transaction is suspended, a new transaction state is created with newTransactionStatus and a call to doBegin is made to start the transaction, which will not be repeated here. Next, look at the PROPAGATION_NESTED:
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() + "]"); } / / the default can be nested transaction the if (useSavepointForNestedTransaction ()) {/ / Create the savepoint within existing Spring - managed transaction, Implemented by TransactionStatus. // Usually uses JDBC 3.0 savepoints.never activates Spring synchronization. DefaultTransactionStatus status = prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); / / create a rollback 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. boolean newSynchronization = (getTransactionSynchronization() ! = SYNCHRONIZATION_NEVER); DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, null); doBegin(transaction, definition); prepareSynchronization(status, definition); return status; }}Copy the code
You can see that if nested transactions are allowed, a DefaultTransactionStatus object (newTransaction is false, indicating that it is not a newTransaction) and a rollback point are created. If nesting is not allowed, a new transaction is created and started. If none of the above criteria is satisfied, propagating the default PROPAGATION_REQUIRED, then DefaultTransactionStatus is returned with newTransaction as false. Is completed is called proceedWithInvocation, then perform the addB method of a class B, if do not have an exception occurs, you will call back to the plane commitTransactionAfterReturning submit addB transactions:
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) { if (txInfo ! = null && txInfo.getTransactionStatus() ! = null) { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } } public final void commit(TransactionStatus status) throws TransactionException { processCommit(defStatus); } private void processCommit(DefaultTransactionStatus status) throws TransactionException { try { boolean beforeCompletionInvoked = false; try { boolean unexpectedRollback = false; prepareForCommit(status); triggerBeforeCommit(status); triggerBeforeCompletion(status); beforeCompletionInvoked = true; if (status.hasSavepoint()) { if (status.isDebug()) { logger.debug("Releasing transaction savepoint"); } / / if it is a nested, not submitted, just will rid the savepoint unexpectedRollback = status. IsGlobalRollbackOnly (); status.releaseHeldSavepoint(); } // If both are PROPAGATION_REQUIRED, the outermost one will walk in, PROPAGATION_REQUIRES_NEW, Else if (status.isnewTransaction ()) {if (status.isdebug ()) {logger.debug("Initiating transaction commit"); } unexpectedRollback = status.isGlobalRollbackOnly(); doCommit(status); } else if (isFailEarlyOnGlobalRollbackOnly()) { unexpectedRollback = status.isGlobalRollbackOnly(); } } } finally { cleanupAfterCompletion(status); }}Copy the code
The primary logic is in the processCommit method. If there is a rollback point, you can see that no transaction is committed, but that the rollback point for the current transaction is removed. In the case of a new transaction, doCommit is called, and only the outermost transaction under the PROPAGATION_REQUIRED attribute and PROPAGATION_REQUIRES_NEW attribute can be committed. After the transaction commits, cleanupAfterCompletion is called to clear the state of the current transaction, and if there are any pending transactions, the pending transaction is resumed via resume (untying the connection to the current thread and setting the previously saved transaction state back). If the current transaction commits normally, then the addA method will commit. If the call addB anomalies, can through the completeTransactionAfterThrowing rollback:
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { if (txInfo ! = null && txInfo.getTransactionStatus() ! = null) { if (logger.isTraceEnabled()) { logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex); } if (txInfo.transactionAttribute ! = null && txInfo.transactionAttribute.rollbackOn(ex)) { try { txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } } } } public final void rollback(TransactionStatus status) throws TransactionException { DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; processRollback(defStatus, false); } private void processRollback(DefaultTransactionStatus status, boolean unexpected) { try { boolean unexpectedRollback = unexpected; try { triggerBeforeCompletion(status); HasSavepoint ()) {if (status.isdebug ()) {logger.debug("Rolling back transaction to savepoint"); } status.rollbackToHeldSavepoint(); } // All revert to PROPAGATION_REQUIRED else if (status.isnewTransaction ()) {if (status.isDebug()) { logger.debug("Initiating transaction rollback"); } doRollback(status); } else { // 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); } 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"); } // 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; } 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 { cleanupAfterCompletion(status); }}Copy the code
The process is the same as the submission process, first determine whether there is a rollback point, if there is a rollback point back to the rollback point and clear the rollback point; If not, it checks whether the transaction is new (the outermost transaction under the PROPAGATION_REQUIRED attribute and the PROPAGATION_REQUIRES_NEW attribute), and rolls back the current transaction. Once the rollback is complete, it is also necessary to clear the current transaction state and restore pending connections. Another important thing to note is that after invoking the rollback logic in a catch, an exception is also thrown through a throw. What does this mean? This means that even for nested transactions, the rollback of the inner transaction causes the rollback of the outer transaction, namely the addA transaction, to follow suit. At this point, the propagation principle of a transaction is analyzed. It is complicated to look into the implementation of each method, but there is a simple way to analyze the effect of each propagation attribute on a transaction. We can replace the inner affairs section equivalent invocation. ProceedWithInvocation methods, such as the above two kind of calls can be as follows:
/ / addA transaction TransactionInfo txInfo = createTransactionIfNecessary (tm, txAttr joinpointIdentification); Object retVal = null; Try {/ / addB transaction TransactionInfo txInfo = createTransactionIfNecessary (tm, txAttr joinpointIdentification); Object retVal = null; try { retVal = invocation.proceedWithInvocation(); } the catch (Throwable ex) {/ / target invocation exception / / transaction rollback completeTransactionAfterThrowing (txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } / / transaction commit commitTransactionAfterReturning (txInfo); } the catch (Throwable ex) {/ / transaction rollback completeTransactionAfterThrowing (txInfo, ex); throw ex; } / / transaction commit commitTransactionAfterReturning (txInfo);Copy the code
Is it easy to figure out the impact between transactions and whether they are committed or rolled back? Let’s look at some examples.
The example analysis
I’m going to addA C class and addC method, and I’m going to call that method in addA.
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null; Try {/ / addB transaction TransactionInfo txInfo = createTransactionIfNecessary (tm, txAttr joinpointIdentification); Object retVal = null; try { b.addB(); } the catch (Throwable ex) {/ / target invocation exception / / transaction rollback completeTransactionAfterThrowing (txInfo, ex); throw ex; } / / transaction commit commitTransactionAfterReturning (txInfo); / / the affairs of addC TransactionInfo txInfo = createTransactionIfNecessary (tm, txAttr joinpointIdentification); Object retVal = null; try { c.addC(); } the catch (Throwable ex) {/ / target invocation exception / / transaction rollback completeTransactionAfterThrowing (txInfo, ex); throw ex; } / / transaction commit commitTransactionAfterReturning (txInfo); } the catch (Throwable ex) {/ / transaction rollback completeTransactionAfterThrowing (txInfo, ex); throw ex; } / / transaction commit commitTransactionAfterReturning (txInfo);Copy the code
The equivalent substitution is the code above. Let’s analyze it separately.
- All are PROPAGATION_REQUIRED: From the above analysis, we know that all three methods are the same connection and transaction, so any exception will be rolled back.
- addBforPROPAGATION_REQUIRES_NEW:
- If an exception is thrown in B, it must be rolled back in B, and then the exception is thrown up, causing the whole transaction in A to roll back;
- If C throws an exception, it’s not hard to see that both C and A will roll back, but B has already committed, so it won’t be affected.
- addCforPROPAGATION_NESTED.addBforPROPAGATION_REQUIRES_NEW:
- If B throws an exception, B rolls back and throws an exception, A rolls back, C does not execute;
- If C throws an exception, it first rolls back to the rollback point and throws the exception, so A also rolls back, but B has already committed and is not affected.
- Exceptions are PROPAGATION_NESTED, but the connection is still the same, and any exception that occurs is rolled back. If you do not want to affect each other, try-catch the exception implementation of the child transaction.
There are many other cases, which are not listed here, that can be easily analyzed using the above analysis methods.
conclusion
This article provides a detailed analysis of how transactions propagate, as well as the isolation level, which is not present in Spring and requires our own analysis Settings based on our database knowledge. Finally, we need to consider the advantages and disadvantages of declarative and programmatic transactions. Declarative transactions are simple but not suitable for long transactions, which consume a lot of connection resources. This is where we need to consider the flexibility of programmatic transactions. In summary, the use of transactions is not always by default. Interface consistency and throughput are directly related to transactions, which can lead to system crashes in severe cases.