Version based on Spring5.3.8

Introduction to Basic Knowledge

Before formally introducing the source code for SpringTx, let’s review the basics

Spring transaction propagation mechanism type

This small segment from segmentfault.com/a/119000002…

PROPAGATION_REQUIRED (default)

  • Support the current transaction, if no current transaction, a new transaction
  • If the current transaction exists, join the current transaction and merge into one transaction

REQUIRES_NEW

  • Create a new transaction and suspend the current transaction if one exists
  • This method commits the transaction independently and is not affected by the caller’s transaction. The parent exception also commits normally

NESTED

  • If the current transaction exists, it will become a child of the parent transaction, and will not commit until the parent transaction ends
  • If there is no current transaction, create a new transaction
  • If it is abnormal, the parent can catch its exception without rollback and commit normally
  • But if the parent is abnormal, it must be rolled back, and this is the sumREQUIRES_NEWThe difference between

SUPPORTS

  • If a transaction exists, join the transaction
  • If there is no current transaction, it will run non-transactionally, which is the same as no write

NOT_SUPPORTED

  • Run in a non-transactional manner
  • Suspend the current transaction, if one exists

MANDATORY

  • Runs in the current transaction, if one exists
  • If there is no current transaction, an exception is thrown, that is, the parent method must have a transaction

NEVER

  • Run in a non-transactional manner and throw an exception if a transaction exists, that is, the parent method must be transaction-free

case

Case a

Outer business: REQUIRED

@Service public class TestServiceImpl implements TestService { @Resource private JdbcTemplate jdbcTemplate; @Resource private TestService2 testService2; @Override @Transactional public String test() { jdbcTemplate.update("INSERT INTO test1(name) values ('zhoujielun')"); testService2.test(); throw new RuntimeException(); }}Copy the code

Inner transaction: REQUIRES_NEW

@Service
public class TestServiceImpl2 implements TestService2 {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public String test() {
        jdbcTemplate.update("INSERT INTO test1(name) values ('zhaoqijun')");
        return "2";
    }

}
Copy the code

The results

The outer layer failed to write data, but the inner layer succeeded

Case 2

The outer transaction does not change, but the inner transaction becomes NESTED

Inner transaction: NESTED

@Service public class TestServiceImpl2 implements TestService2 { @Resource private JdbcTemplate jdbcTemplate; @Override @Transactional(propagation = Propagation.NESTED) public String test() { jdbcTemplate.update("INSERT INTO test1(name) values ('zhaoqijun')"); return "2"; }}Copy the code

The results

Failed to write all inner and outer layers

Source code analysis

Before the case analysis begins, we first determine the entrance of source code analysis, as we all know, SpringAOP is based on dynamic proxy implementation, we can easily determine the core entrance to Spring transactions, TransactionInterceptor, for interceptor, Look at his invoke method, of course

The core logic, called invokeWithinTransaction, can be found in the Invoke method, which is the implementation entry point for SpringTx.

Source code analysis we first from the first case, the first code into the outer transaction. Starting from the entry, invokeWithinTransaction is divided into several core parts, as follows

The third is to completeTransactionAfterThrowing note here because the outer transaction is throwing an exception. This a few steps first to see the first createTransactionIfNecessary, his role is to get one

TransactionInfo, this object is the core object that runs through the entire Spring transaction, keep this name in mind, so what are the core properties in this object? Note that not all properties are listed here

There are two properties, one is TransactionStatus object, and one is oldTransactionInfo, and what do they do? Let’s just press no table. See first createTransactionIfNecessary in logic.

The first step is getTransaction. As the name suggests, this is how you get the transaction object, so where do you get it from? In two steps

The first is doGetTransaction

The core point of doGetTransaction is the doGetResource method, where the core logic is to call the GET method of resources to get a map, and then fetch the value from that map

org.springframework.transaction.support.TransactionSynchronizationManager#doGetResource

So there are a couple of core questions that arise from this

  1. What are resources?
  2. What is the map, the key and the value that we pulled out

If you look at the source code, you can see that resources is a Threadlocal, and the object stored is a Map. So what is the Map

If we Debug the source code, you can see

  • The key is the Datasource object
  • Value is a ConnectionHolder object

The core parameters in the ConnectionHolder object are as follows

So, to summarize, this resource is actually a thread dimension that stores a mapping between a data source object and a link object. Is the result of calling doGetResource the same as this ConnectionHolder? Back in our case, this is the first time the outer layer is called, so the answer is no, because at this point in the thread, no transaction has been created, and the resource is fetching an empty object.

After doGetTransaction is executed, the second step, isExistingTransaction, is to determine whether a transaction already exists by name

As described above, the transaction has not yet been created, so this is false, so we start to create a transaction, creating a transaction is divided into several core steps, of which the most important three are

  1. New Connection and New ConnectionHolder(newCon)
  2. SetAutoCommit (false) Starts the transaction
  3. TransactionSynchronizationManager#bindResource

The first step creates the connection. The third step binds the datasource object to the thread as the key and the ConnectionHolder as the value

After this method executes, the transaction is created and the connection is bound to the thread, and the process is reviewed

The created transaction object, essentially the connection object, is encapsulated in a TransactionStatus object, which is then encapsulated in a TransactionInfo in the next prepareTransactionInfo and bound to the thread

GetTransaction and prepareTransactionInfo were performed after createTransactionIfNecessary are performed, and ultimately get TransactionInfo object, layer upon layer relationship indirectly holds the connection object

This may seem complicated, but it’s really just a matter of remembering that TransactionInfo already holds the connection object. When the prepareTransactionInfo is finished, it triggers the invocation of the actual method, which is the execution of the business code

Let’s review the logic of the outer layer

@Service public class TestServiceImpl implements TestService { @Resource private JdbcTemplate jdbcTemplate; @Resource private TestService2 testService2; @Override @Transactional public String test() { jdbcTemplate.update("INSERT INTO test1(name) values ('zhoujielun')"); testService2.test(); throw new RuntimeException(); }}Copy the code

When the outer layer finishes writing the data, it triggers the inner layer’s transaction call. Again, the inner layer starts from the same entry, called invokeWithinTransaction

Is familiar with the name of the method, the first is createTransactionIfNecessary

Again, it continues to be getTransaction

Moving on to doGetTransaction, let’s review the code again

In the outer code logic, we know that when we fetched the connection object with the Datasource object as the key, the first time we fetched the Datasource object, the object was empty. So if I take the inner layer again, is it still empty?

The answer is no. This is the first connection object that was built. So here’s a new question, is it necessary to use it? Let’s look at isExistingTransaction for getTransaction. Since we already have a transaction, this method returns true

So at this moment because it is true, and the outer layer of the logic is different, is entering handleExistingTransaction, dealing with the already existing. This is where the propagation feature comes in.

Since the propagation property of the inner layer at this point is PROPAGATION_REQUIRES_NEW, as a refresher, a new transaction will actually be started depending on the property.

** Then the question comes, new business opened, where to the old business?? ** This leads to step 1, suspend method

Suspend is divided into two parts: doUnbindResource, SuspendedResourcesHolder, SuspendedResourcesHolder You can easily figure out doUnbindResource, which is unbinding the ConnectionHolder from the previous thread

After unbinding, the deleted return value, the ConnectionHolder object, is temporarily stored in the SuspendedResourcesHolder.

And who is this SuspendedResourcesHolder? It can also be viewed as a property of TransactionInfo that stores suspended objects

SuspendedResourcesHolder, SuspendedResourcesHolder, SuspendedResourcesHolder, SuspendedResourcesHolder, SuspendedResourcesHolder, SuspendedResourcesHolder, SuspendedResourcesHolder, SuspendedResourcesHolder, SuspendedResourcesHolder, SuspendedResourcesHolder Same as the outer layer.

According to the case code, the inner layer executes normally here and commits the transaction.

@Service
public class TestServiceImpl2 implements TestService2 {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public String test() {
        jdbcTemplate.update("INSERT INTO test1(name) values ('zhaoqijun')");
        return "2";
    }

}
Copy the code

So the third part, will come to invokeWithinTransaction commitTransactionAfterReturning

CommitTransactionAfterReturning, obviously, is submitted after the return to the execution of the transaction, the logic is divided into two parts

The first part is to actually commit the transaction. The important part is the second part, where the transaction is transferred again, and the transaction in the current thread is switched from the inner transaction to the outer transaction

The specific logic is to take the ConnectionHolder from the SuspendedResourcesHolder in the transaction object and bind it back to the thread.

Once the transaction has been restored to the outer transaction, it is time to proceed to execute the outer transaction. Let’s look at the code for the outer transaction

@Service public class TestServiceImpl implements TestService { @Resource private JdbcTemplate jdbcTemplate; @Resource private TestService2 testService2; @Override @Transactional public String test() { jdbcTemplate.update("INSERT INTO test1(name) values ('zhoujielun')"); testService2.test(); throw new RuntimeException(); }}Copy the code

Complete after lining method, continue to implement, is to throw an exception, the exception will cause the transaction rollback, so will trigger a new logic, is another part of invokeWithinTransaction completeTransactionAfterThrowing

CompleteTransactionAfterThrowing is divided into two parts

  1. doRollback
  2. doResume

Step 1 is to roll back the business logic. Note that the inner and outer layers are different transactions, and the inner layer transaction has been committed, so the outer layer transaction is rolled back. As for step two, since this is already the outermost transaction, there is nothing to doResume, so ignore it.

At this point, the source code analysis of case 1 is finished. Summarize the process

The only difference between case 2 and Case 1 is that the propagation property of the inner layer changed from REQUIRES_NEW to Nested. we review the entire logic of Case 1

IsExistingTransaction called in the inner layer.

So in this case, we skip the outer transaction and go directly to the analysis of the inner transaction. The entry is also invokeWithinTransaction

The first step is the same createTransactionIfNecessary, here, the first obtained from the ThreadLocal outer transaction, after enter isExistingTransaction, obviously, The result of isExistingTransaction is true

Therefore, the processing of the existing outer transaction continues, and the propagation property at this time is nested-out

Recall that in case 1, when the inner transaction propagation feature was REQUIRES_NEW, the inner logic was to suspend the old transaction and create a new one. We know that if the outer transaction is abnormal, the inner transaction must be rolled back. This is the difference between REQUIRES_NEW and nested_transaction. So how does this work in Spring?

So the core step is, createAndHoldSavepoint, using the outer transaction, sets a savePoint, so we’re actually using Mysql’s savePoint here.

Instead of creating a new transaction, a savePoint is set on top of the outer transaction.

Set after the savePoint, began the next step, the implementation of the corresponding business logic, after the execution, as a case, came to the key step, is to commit the transaction commitTransactionAfterReturning

Instead of committing the transaction directly in case 1, this determines whether the current transaction has a savePoint, which is obviously true

ReleaseHeldSavepoint is first called to release the savePoint in the current transaction

Then the outer transaction continues, but it is the same transaction, so this logic can be ignored. At this point, the inner transaction of Case 2 is finished and goes back to the outer transaction

@Service public class TestServiceImpl implements TestService { @Resource private JdbcTemplate jdbcTemplate; @Resource private TestService2 testService2; @Override @Transactional public String test() { jdbcTemplate.update("INSERT INTO test1(name) values ('zhoujielun')"); testService2.test(); throw new RuntimeException(); }}Copy the code

The outer layer throws an exception and then triggers a rollback. The logic here is exactly the same as in case 1. Note, however, that because in case 2, the inner and outer layers are the same transaction, the logic in the inner layer is also rolled back, unlike in case 1.

So to summarize the overall process of case two

Reviewing the entire case, there are two core parts to the Spring transaction process

  1. Transaction building and thread binding
  2. The handover of inner and outer transactions, the suspension and recovery of transactions

Here also leave you three questions to think about, interested in the source code can do their own analysis

  1. How does transaction switching work in the case of multiple data sources
  2. How does the inner transaction differ if it is asynchronous
  3. What happens if the inner layer reports an error and the outer layer executes successfully