[giant's shoulder] detailed explanation of Spring affairs

Posted by DarkEden on Thu, 03 Feb 2022 11:04:01 +0100

Introduction

Many coder s use database transactions without understanding the principle of transactions or even the basic concepts. It is very easy to make mistakes and write some code that they can't control. Many articles on the Internet are either concepts, a little source code, or a little test verification, which are not enough to fully understand the transaction, so this article appears

The full text is based on Mysql innodb engine. Mysql official document , recommended book: Mysql technology insider InnoDB storage engine

1. Worship God

The leader of spring transaction is Juergen Hoeller. Yuergen is confused... First make a familiar face. He wrote almost all the spring transaction code. Read the source code, worship God first, master the style of his source code, and it will be much smoother to read. In the last section, let's summarize the code style of the great God

2. Preliminary services

2.1 definition of transaction

Transaction is one of the important characteristics that distinguish database from file system. At present, the internationally recognized database design principle is ACID feature, which is used to ensure the correct execution of database transactions. The transactions in Mysql's innodb engine fully comply with the ACID feature

The hierarchical overview of Spring's support for transactions is shown below

2.2 ACID characteristics of transactions
  1. Atomicity: a transaction must be regarded as an indivisible minimum unit of work. All operations in the whole transaction must be committed successfully or rolled back
    It mainly involves InnoDB transactions. Related features: transaction commit, rollback, information table

  2. consistency: the database always transitions from one consistent state to another. Before and after the start of the transaction, the integrity constraints of the database are not broken. For example, if uniqueness is violated, the transaction must be revoked and returned to the initial state
    It mainly involves internal InnoDB processing to protect data from crash. Related features: Double write buffer and crash recovery

  3. isolation: the object of each read-write transaction can be separated from the operation object of other transactions, that is, it is invisible to other transactions before the transaction is committed, which is usually implemented by internal locking
    It mainly involves transactions, especially the transaction isolation level. Related features: isolation level and the underlying implementation details of innodb lock

  4. Persistence: once a transaction is committed, its changes will be permanently saved to the database
    It involves the interaction between MySQL software features and specific hardware configuration. Relevant features: four configuration items: Double write buffer switch, transaction commit refresh log level, binlog synchronization frequency and table file; Write cache, operating system support for fsync(), backup strategy, etc

2.3 transaction attributes

To ensure the ACID characteristics of transactions, spring defines six attributes for transactions, corresponding to declarative transaction annotation (org.springframework.transaction.annotation.Transactional) @ Transactional(key1=,key2 =...)

Transaction name: you can manually specify the name of the transaction. When there are multiple transactions, you can distinguish which transaction to use. Corresponding to the attribute value and transactionManager in the annotation
Isolation level: in order to solve the problems prone to occur in the database, the hierarchical locking processing strategy is adopted. Attribute isolation in corresponding annotation
Timeout time: defines how long a transaction execution process is timed out so that it can be rolled back after timeout. It can prevent long-running transactions from occupying resources. Corresponding to the attribute timeout in the annotation
Read only: indicates that this transaction only reads data but does not update data, which can help the database engine optimize transactions. Corresponding to the attribute readOnly in the annotation
Propagation mechanism: defines the propagation characteristics of transactions. There are seven types. Corresponding to the attribute propagation in the annotation
Rollback mechanism: defines the rollback policy when an exception is encountered. Corresponding to the attributes rollbackFor, noRollbackFor, rollbackForClassName and noRollbackForClassName in the annotation
Among them, the isolation level and transmission mechanism are relatively complex. Let's taste one product in detail

2.3.1 isolation level

This one is complex. We can see it from three perspectives: three kinds of error phenomena, the underlying technical support of mysql, and hierarchical processing strategy. We must take a good look at this section. It has begun to involve the core principles

  1. Phenomenon (three problems)
    **Dirty read: * * transaction a updates the record but fails to commit, and transaction B queries the uncommitted record of A
    Non repeatable read: transaction A reads the data once. At this time, transaction B updates or deletes the data, and transaction A queries the data again, which is inconsistent
    Phantom read: transaction A reads once. At this time, transaction B inserts A piece of data. Transaction A queries again, and there are too many records

  2. Underlying support of mysql (IndoDB transaction model) (consistent unlocked read VS locked read)

Airline ticket on official website: InnoDB Transaction Model

Two kinds of reads: in MVCC, read operations can be divided into two types: Snapshot read and current read.

Snapshot read:
Normal select
Current read:
select * from table where ? lock in share mode; (with S lock)
select * from table where ? for update; (with X lock)
The current read (with X lock) will be performed before the insert, update and delete operations
The first two types of locked reads need to be explicitly used by the user, and the last one is automatically added

consistent nonlocking read: it means that the InnoDB storage engine reads the data of the row in the database at the current execution time through multi versioning. If the read row is performing DELETE or update operation, it means that the read operation will not wait for the release of row lock. On the contrary, InnoDB will read a snapshot data of the row

The above shows the non locked read of InnoDB storage engine consistency. It is called an unlocked read because there is no need to wait for the release of the X lock on the accessed row. Snapshot data refers to the data of the previous version of the line. This implementation is completed through the undo segment. Undo is used to rollback data in transactions, so the snapshot data itself has no additional overhead. In addition, reading the snapshot data does not need to be locked, because there is no transaction to modify the historical data

Locked read (current read): InnoDB supports two types of locked read for the SELECT statement: (1): SELECT... FOR UPDATE adds an exclusive lock (x lock) to the read row, and other transactions cannot add any lock to the locked row; (2) : SELECT... LOCK IN SHARE MODE add a shared lock (S lock) to the read row. Other transactions can add an S lock. The X lock will block the wait

Note: both locks must be in a transaction. The transaction commits and the lock is released. Therefore, you must start a transaction with begin or start transaction, or simply set autocommit=0 to turn off automatic submission (mysql defaults to 1, that is, submit immediately after executing sql)

  1. Hierarchical processing strategy (four isolation levels)

Official website description: InnoDB uses different locking policies to support each transaction isolation level. For the operation of key data (follow the ACID principle), you can use strong consistency (default Repeatable Read). For less important data operations, you can use Read Committed/Read Uncommitted. Serializable implements more stringent rules than rereadability for special scenarios: XA transactions, troubleshooting concurrency and deadlock problems

Four isolation levels:
1. Read Uncommitted: it is possible to read uncommitted data of other transactions. Can not solve: dirty reading, non repeatable reading, unreal reading
2. Read Committed: a transaction can only see the changes made by the committed transaction. Can't solve: can't repeat reading, unreal reading
Select... from: the consistent unlocked read data snapshot (MVCC) is the latest version, but other transactions may have new commit ments, so the same select may return different results
select... From for update: record lock
3. Repeatable Read: cannot solve: unreal read
select... from: multiple consistent unlocked reads in the same transaction. Take the snapshot Version (MVCC) established at the first reading to ensure repeatable reads in the same transaction The problem of illusory reading in a narrow sense has been solved. (Db inserts data, but cannot read)
select... from for update (FOR UPDATE or LOCK IN SHARE MODE), UPDATE, and delete: next key lock
For unique indexes with unique search criteria, InnoDB only locks the index records found (next key lock is reduced to record lock)
For other non index or non unique indexes, InnoDB will lock the scanned index range and use next key locks to block the insert operation of other session s on the gap, - completely solve the generalized unreal reading problem. (no data inserted in DB)
4. Serializable: This is the highest isolation level. It adds a LOCK IN SHARE MODE to each read data row. At this level, it may lead to a large number of timeout phenomena and lock competition, which is mainly used for distributed transactions

2.3.2 communication mechanism

org. springframework. Under the transaction package, there is a transaction definition interface, TransactionDefinition, which defines seven transaction propagation mechanisms. Many people misinterpret the propagation mechanism from the concept, so they specially translate the source code, and the notes are as follows:
1. PROPAGATION_REQUIRED: supports the current transaction; If not, create a new one. Similar to EJB transaction attribute with the same name. This is usually the default setting of the transaction definition, which usually defines the transaction synchronization scope. There is a transaction: 1 An external transaction is added and rolled back abnormally; 2. Create a new transaction without an external transaction and roll back abnormally
2. PROPAGATION_SUPPORTS: supports the current transaction: if there is no transaction, it will be executed in a non transactional manner. Similar to EJB transaction attribute with the same name. Note: for transaction managers with transaction synchronization, deployment_ Supports is slightly different from no transaction because it may define synchronization within a transaction. Therefore, the same resources (JDBC Connection, Hibernate Session, etc.) will be shared in the whole specified range. Note that the exact behavior depends on the actual synchronization configuration of the transaction manager!
Use propagation carefully_ SUPPORTS! In particular, don't rely on promotion_ Required or promotion_ REQUIRES_ New, in promotion_ Within the scope of supports (this may cause synchronization conflicts at run time). If this nesting is unavoidable, make sure that the transaction manager is properly configured (usually switching to "synchronization on actual transactions"). At most one transaction exists: 1 An external transaction is added and rolled back abnormally; 2. No external transaction, no internal transaction, no rollback for exceptions
3. PROPAGATION_MANDATORY: supports the current transaction; Throw an exception if the current transaction does not exist. Similar to EJB transaction attribute with the same name. Note: Promotion_ Transaction synchronization within the mandatory scope is always driven by surrounding transactions. At most one transaction exists: 1 External transactions join and roll back abnormally; 2. There is no external transaction, and the exception cannot be rolled back
4. PROPAGATION_REQUIRES_NEW: create a new transaction. If there is a current transaction, suspend the current transaction. Similar to EJB transaction attribute with the same name. Note: the actual transaction suspension will not be out of the box on all transaction managers. This applies especially to jtatactionmanager, which requires the support of transaction manager.
PROPAGATION_ REQUIRES_ The new scope always defines its own transaction synchronization. The existing synchronization will be suspended and resumed appropriately. There may be 1-2 transactions: 1 The external transaction is suspended. Create a new exception and roll back your own transaction 2 There is no external transaction. Create a new exception and only roll back the new transaction
5. PROPAGATION_NOT_SUPPORTED: the current transaction is not supported. There is a transaction to suspend the current transaction; Always execute in a non transactional manner. Similar to EJB transaction attribute with the same name. Note: the actual transaction suspension will not be out of the box on all transaction managers. This applies especially to jtatactionmanager, which requires the support of transaction manager. Transaction synchronization in deployment_ NOT_ Not available in the supported range. The existing synchronization will be suspended and resumed appropriately. At most one transaction exists: 1 The external transaction is suspended, and the external exception is rolled back; Internal non transaction, exception does not roll back 2 No external transaction, no internal transaction, no rollback for exceptions
6. PROPAGATION_NEVER: current transaction is not supported; Throw an exception if the current transaction exists. Similar to EJB transaction attribute with the same name. Note: transaction synchronization is in deployment_ Not available in the never range. At most one transaction exists: 1 External transaction, external exception rollback; Internal non transaction does not roll back 2 External non transaction, internal non transaction, exception not rolled back
7. PROPAGATION_NESTED: if the current transaction exists, it will be executed in a nested transaction. If there is no current transaction, it is similar to PROPAGATION_REQUIRED (create a new one). There is no similar function in EJB. Note: the actual creation of nested transactions is only valid for a specific transaction manager. Out of the box, this only applies to the data source transaction manager (JDBC 3.0 driver). Some JTA providers may also support nested transactions. There is a transaction: 1 There are external transactions, nested transactions create savepoints, and external exceptions roll back all transactions; Internal nested transaction exception rollback to savepoint; 2. There is no external transaction, a new transaction is created internally, and the internal exception is rolled back

3 simple example

In Spring, transactions can be implemented in two ways:

  1. Programming transaction management: using the underlying source code, programming transaction management can achieve more fine-grained transaction control. Spring recommends using TransactionTemplate, a typical template mode
  2. Declarative transaction management: add @ Transactional annotation and define propagation mechanism + rollback strategy. Based on the Spring AOP implementation, the essence is to intercept before and after the method, then create or join a transaction before the target method starts, and commit or roll back the transaction according to the execution after the target method is executed
3.1 simple example

**Requirement: * * create a user balance table when creating a user. If the user balance creation fails and an exception is thrown, the user table will also be rolled back, that is to ensure that "new user + new user balance" is successfully rolled back together

3.2 declarative transaction management

As shown in the following figure, you only need to use the service In impl layer, add @ Transactional annotation on the business method, define the propagation mechanism of transaction as REQUIRED (if this parameter is not written, it is REQUIRED by default), and roll back when Exception exception is encountered

Under the REQUIRED propagation mechanism: there is a join transaction, and there is no new transaction created. It ensures that all database operations in the current method are in one physical transaction. When an exception is encountered, the whole business method will be rolled back together

 /**
     * Create user and create account balance
      *
      * @param name
      * @param balance
      * @return
      */
     @Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class)
     @Override
     public void addUserBalanceAndUser(String name, BigDecimal balance) {
         log.info("[addUserBalanceAndUser] begin!!!");
         //1. New users
         userService.addUser(name);
         //2. New user balance
         UserBalance userBalance = new UserBalance();
         userBalance.setName(name);
         userBalance.setBalance(new BigDecimal(1000));
         this.addUserBalance(userBalance);
         log.info("[addUserBalanceAndUser] end!!!");
     }
3.3 programmed transaction management

For programming transaction management, we use the TransactionTemplate recommended by Spring. Because I use the annotation configuration of spring cloud, the implementation uses the automatic configuration class to configure the bean of TransactionTemplate type. When using, you can directly inject beans for use (of course, the same is true for old-fashioned xml configuration). as follows

 /**
      * Create user and create account balance (manual transaction without result)
      *
      * @param name
      * @param balance
      * @return
      */
     @Override
     public void addUserBalanceAndUserWithinTT(String name, BigDecimal balance) {
         //Implement a transaction callback with no return value
         transactionTemplate.execute(new TransactionCallbackWithoutResult() {
             @Override
             protected void doInTransactionWithoutResult(TransactionStatus status) {
                 try {
                     log.info("[addUserBalanceAndUser] begin!!!");
 
                     //1. New users
                     userService.addUser(name);
                     //2. New user balance
                     UserBalance userBalance = new UserBalance();
                     userBalance.setName(name);
                     userBalance.setBalance(new BigDecimal(1000));
                     userBalanceRepository.insert(userBalance);
                     log.info("[addUserBalanceAndUser] end!!!");
                     //Note: after catching the exception, set setRollbackOnly, otherwise the transaction will not roll. Of course, if you don't need to handle exceptions yourself, don't catch
                 } catch (Exception e) {
                     // Abnormal rollback
                     status.setRollbackOnly();
                     log.error("Abnormal rollback!,e={}",e);
                 }
 
             }
         });
     }

be careful:

  1. try catch and transactiontemplate are not needed Execute catches the exception and rolls it back
  2. If there are business exceptions that need special handling, remember: status setRollbackOnly(); Identify as rollback

4. Detailed source code

4.1 programmatic transaction template

For programming transactions, Spring has provided us with the template class TransactionTemplate, which can be used conveniently, as shown in the following figure

The full pathname of the transactiontemplate is: org springframework. transaction. support. TransactionTemplate. Look at the package name. This is the Template class of spring for transactions. (spring is all kinds of templates...) look at the class diagram first

At first glance, yo Xi has implemented the two interfaces of TransactionOperations and InitializingBean (those familiar with Spring source code know that this InitializingBean is an old routine). Let's take a look at the interface source code as follows

 public interface TransactionOperations {
 
     /**
      * Execute the action specified by the given callback object within a transaction.
      * <p>Allows for returning a result object created within the transaction, that is,
      * a domain object or a collection of domain objects. A RuntimeException thrown
      * by the callback is treated as a fatal exception that enforces a rollback.
      * Such an exception gets propagated to the caller of the template.
      * @param action the callback object that specifies the transactional action
      * @return a result object returned by the callback, or {@code null} if none
      * @throws TransactionException in case of initialization, rollback, or system errors
      * @throws RuntimeException if thrown by the TransactionCallback
      */
     <T> T execute(TransactionCallback<T> action) throws TransactionException;
 
 }
 
 public interface InitializingBean {
 
     /**
      * Invoked by a BeanFactory after it has set all bean properties supplied
      * (and satisfied BeanFactoryAware and ApplicationContextAware).
      * <p>This method allows the bean instance to perform initialization only
      * possible when all bean properties have been set and to throw an
      * exception in the event of misconfiguration.
      * @throws Exception in the event of misconfiguration (such
      * as failure to set an essential property) or if initialization fails.
      */
     void afterPropertiesSet() throws Exception;
 
 }

As shown in the figure above, the TransactionOperations interface is used to execute the callback method of transactions. InitializingBean is a typical spring bean initialization process (air ticket: Spring IOC (IV) summary and sublimation )The reserved interface of is specially used to execute the method when the bean properties are loaded

Back to the point, what does the impl method of the two interfaces of TransactionTemplate do?

     @Override
     public void afterPropertiesSet() {
         if (this.transactionManager == null) {
             throw new IllegalArgumentException("Property 'transactionManager' is required");
         }
     }
 
 
     @Override
     public <T> T execute(TransactionCallback<T> action) throws TransactionException {
       // Internally encapsulated transaction manager
         if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
             return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
         }// You need to manually obtain the transaction, execute the method, and commit the transaction manager
         else {// 1. Get transaction status
             TransactionStatus status = this.transactionManager.getTransaction(this);
             T result;
             try {// 2. Execute business logic
                 result = action.doInTransaction(status);
             }
             catch (RuntimeException ex) {
                 // Application runtime exception - > rollback
                 rollbackOnException(status, ex);
                 throw ex;
             }
             catch (Error err) {
                 // Error exception - > rollback
                 rollbackOnException(status, err);
                 throw err;
             }
             catch (Throwable ex) {
                 // Unknown exception - > rollback
                 rollbackOnException(status, ex);
                 throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
             }// 3. Transaction submission
             this.transactionManager.commit(status);
             return result;
         }
     }

As shown in the above figure, in fact, afterpropertieset only verifies that the transaction manager is not empty, and execute() is the core method. The main steps of execute are as follows:

  1. getTransaction() get transaction
  2. doInTransaction() executes business logic. Here is the user-defined business code. If there is no return value, it is doInTransactionWithoutResult()
  3. commit() transaction commit: call the commit of AbstractPlatformTransactionManager, rollback onexception() exception rollback: call the rollback() of AbstractPlatformTransactionManager, transaction commit rollback
4.2 declarative transaction
4.2.1 AOP related concepts

Declarative transactions use Spring AOP, which is aspect oriented programming
The core concepts of AOP are as follows:
1. Advice: it defines the aspect (a aspect refined from the logic required in all business codes) what to do and when to use. For example: Before notification, After notification, After returning notification, After throwing exception notification, Around notification
2. Joint point: there are usually multiple points that can be inserted into the section during program execution. For example, when calling a method or throwing an exception
3. Pointcut: the pointcut defines the connection point. The pointcut contains multiple connection points, that is, where to use the notification Usually specify class + method or regular expression to match class and method names
4. Aspect: aspect = notification + pointcut, i.e. when+where+what, when, where and what
5. Introduction: allows us to add new methods or properties to existing classes
6. Weaving: weaving is the process of applying the facet to the target object and creating a new proxy object

4.2.2 declarative transactions

The declarative transaction calls the process as a whole, and two lines can be drawn:

  1. Use proxy mode to generate proxy enhancement classes
  2. According to the agent transaction management configuration class, configure the weaving of transactions, enhance the surrounding before and after business methods, and add some transaction related operations. For example, get transaction properties, commit transactions, and roll back transactions

The process is as follows:

Declarative transactions use @ Transactional annotation, so let's start with the automatic configuration loading when the spring boot container is started (detailed explanation of spring boot container startup). At / meta-inf / spring Find in the configuration file in factories, as shown in the following figure

Load 2 automatic configuration classes about transactions:

org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,

jta let's not look at it. Let's take a look at the automatic configuration class of TransactionAutoConfiguration:

@Configuration
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, 
        HibernateJpaAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class,
        Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public TransactionManagerCustomizers platformTransactionManagerCustomizers(
           ObjectProvider<List<PlatformTransactionManagerCustomizer<?>>> customizers) {
        return new TransactionManagerCustomizers(customizers.getIfAvailable());
    }

    @Configuration
    @ConditionalOnSingleCandidate(PlatformTransactionManager.class)
    public static class TransactionTemplateConfiguration {

        private final PlatformTransactionManager transactionManager;

        public TransactionTemplateConfiguration(
               PlatformTransactionManager transactionManager) {
            this.transactionManager = transactionManager;
        }

        @Bean
        @ConditionalOnMissingBean
        public TransactionTemplate transactionTemplate() {
            return new TransactionTemplate(this.transactionManager);
        }
    }

    @Configuration
    @ConditionalOnBean(PlatformTransactionManager.class)
    @ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
    public static class EnableTransactionManagementConfiguration {

        @Configuration
        @EnableTransactionManagement(proxyTargetClass = false)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = false)
        public static class JdkDynamicAutoProxyConfiguration {

        }
 
        @Configuration
        @EnableTransactionManagement(proxyTargetClass = true)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
        public static class CglibAutoProxyConfiguration {
 
        }
 
    }
 
}

TransactionAutoConfiguration mainly focuses on:

  1. 2 class annotations
    @ConditionalOnClass(PlatformTransactionManager.class) means that the automatic configuration takes effect when the PlatformTransactionManager class is included in the class path. This class is the core package of spring transactions and must be introduced
    @Autoconfigurareafter ({jtaautoconfiguration.class, hibernatejpaautoconfiguration.class, datasourcetransactionmanagerautoconfiguration.class, neo4jdataautoconfiguration. Class}), this configuration takes effect only after the four configuration classes in brackets

  2. 2 internal classes
    TransactionTemplateConfiguration transaction template configuration class:
    @ConditionalOnSingleCandidate(PlatformTransactionManager.class) takes effect only when a PlatformTransactionManager bean can be uniquely identified
    @ConditionalOnMissingBean if no TransactionTemplate bean is defined, a

EnableTransactionManagementConfiguration enable transaction manager configuration class:
@ConditionalOnBean(PlatformTransactionManager.class) takes effect when there is a PlatformTransactionManager bean
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class) takes effect only when there is no custom Abstract transaction manager configuration class. (that is, the user-defined Abstract transaction manager configuration class will take precedence. If not, use this default transaction manager configuration class)

EnableTransactionManagementConfiguration supports two proxy methods:
1. JdkDynamicAutoProxyConfiguration:
1. @EnableTransactionManagement(proxyTargetClass = false), that is, proxyTargetClass = false indicates that it is a JDK dynamic agent. It supports interface oriented agent
2. @ConditionalOnProperty(prefix = "spring.aop", name = "proxy target class", havingValue = "false", matchIfMissing = false), i.e. spring aop. It will take effect when proxy target class = false, and it will not take effect without this configuration
2. CglibAutoProxyConfiguration:
1. @EnableTransactionManagement(proxyTargetClass = true), that is, proxyTargetClass = true identifies that Cglib agent supports subclass inheritance agent
2. @ConditionalOnProperty(prefix = "spring.aop", name = "proxy target class", havingValue = "true", matchIfMissing = true), i.e. spring aop. It takes effect when proxy target class = true, and if there is no such configuration, it takes effect by default

Note that there is no configuration by default and the Cglib agent is used. Note: the @ Transactional annotation supports adding directly to the class
Well, after reading so many configuration classes, I finally came to the annotation @ EnableTransactionManagement

 @Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @Import(TransactionManagementConfigurationSelector.class)
 public @interface EnableTransactionManagement {
 
     //proxyTargetClass = false indicates that the JDK dynamic proxy supports interface proxy. true indicates that Cglib proxy supports subclass inheritance proxy.
     boolean proxyTargetClass() default false;
 
     //Transaction notification mode (aspect weaving mode), default proxy mode (interceptors calling each other in the same class will not take effect), and enhanced AspectJ can be selected
     AdviceMode mode() default AdviceMode.PROXY;
 
     //When there are multiple notifications on the connection point, they are sorted. The default is the lowest. The higher the value, the lower the priority.
     int order() default Ordered.LOWEST_PRECEDENCE;
 
 }

Focus on the class annotation @ Import(TransactionManagementConfigurationSelector.class)

The class diagram of TransactionManagementConfigurationSelector is as follows:

As shown in the figure above, the TransactionManagementConfigurationSelector inherits from the AdviceModeImportSelector and implements the ImportSelector interface

 public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {
 
     /**
      * {@inheritDoc}
      * @return {@link ProxyTransactionManagementConfiguration} or
      * {@code AspectJTransactionManagementConfiguration} for {@code PROXY} and
      * {@code ASPECTJ} values of {@link EnableTransactionManagement#mode()}, respectively
      */
     @Override
     protected String[] selectImports(AdviceMode adviceMode) {
         switch (adviceMode) {
             case PROXY:
                 return new String[] {AutoProxyRegistrar.class.getName(), ProxyTransactionManagementConfiguration.class.getName()};
             case ASPECTJ:
                 return new String[] {TransactionManagementConfigUtils.TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME};
             default:
                 return null;
         }
     }
 
 }

As shown in the figure above, the selectImports method will eventually be executed to import the classes to be loaded. We only look at the two classes loaded in the proxy mode: autoproxyregister and proxytransactionmanagementconfiguration
Autoproxyregister: register an infrastructure advisor autoproxycreator component in the container; After the object is created, the post processor mechanism is used to wrap the object and return a proxy object (enhancer). The proxy object execution method is called by the interceptor chain
ProxyTransactionManagementConfiguration: a configuration class that defines a transaction enhancer

  • AutoProxyRegistrar
    First, autoproxyregister implements the importbeandefinitionregister interface and replicates the registerBeanDefinitions method. The source code is as follows
 public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
         boolean candidateFound = false;
         Set<String> annoTypes = importingClassMetadata.getAnnotationTypes();
         for (String annoType : annoTypes) {
             AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annoType);
             if (candidate == null) {
                 continue;
             }
             Object mode = candidate.get("mode");
             Object proxyTargetClass = candidate.get("proxyTargetClass");
             if (mode != null && proxyTargetClass != null && AdviceMode.class == mode.getClass() &&
                     Boolean.class == proxyTargetClass.getClass()) {
                 candidateFound = true;
                 if (mode == AdviceMode.PROXY) {//proxy pattern
                     AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                     if ((Boolean) proxyTargetClass) {//In case of CGLOB subclass proxy mode
                         AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
                         return;
                     }
                 }
             }
         }
         if (!candidateFound) {
             String name = getClass().getSimpleName();
             logger.warn(String.format("%s was imported but no annotations were found " +
                     "having both 'mode' and 'proxyTargetClass' attributes of type " +
                     "AdviceMode and boolean respectively. This means that auto proxy " +
                     "creator registration and configuration may not have occurred as " +
                     "intended, and components may not be proxied as expected. Check to " +
                     "ensure that %s has been @Import'ed on the same class where these " +
                     "annotations are declared; otherwise remove the import of %s " +
                     "altogether.", name, name, name));
         }
     }

Agent mode: aopconfigutils registerAutoProxyCreatorIfNecessary(registry);

The final call is: registerorexcalateapcasrequired (infrastructure advisor autoproxycreator. Class, registry, source); Infrastructure enhanced automatic proxy constructor

 private static BeanDefinition registerOrEscalateApcAsRequired(Class<?> cls, BeanDefinitionRegistry registry, Object source) {
         Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
     //If the current registrar contains internalAutoProxyCreator
         if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {//org.springframework.aop.config.internalAutoProxyCreator internal automatic proxy constructor
             BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
             if (!cls.getName().equals(apcDefinition.getBeanClassName())) {//If the current class is not internalAutoProxyCreator
                 int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
                 int requiredPriority = findPriorityForClass(cls);
                 if (currentPriority < requiredPriority) {//If the subscript is larger than the existing internal automatic proxy constructor, the smaller the index, the higher the priority. Infrastructure advisor autoproxycreator index = 0, the minimum requiredpriority, and do not enter
                     apcDefinition.setBeanClassName(cls.getName());
                 }
             }
             return null;//Direct return
         }//If the current registrar does not contain internalAutoProxyCreator, the current class is used as the root definition
         RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
         beanDefinition.setSource(source);
         beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);//Highest priority
         beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
         registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
         return beanDefinition;
     }

As shown in the figure above, APC_ PRIORITY_ The list is as follows:

 /**
      * Stores the auto proxy creator classes in escalation order.
      */
     private static final List<Class<?>> APC_PRIORITY_LIST = new ArrayList<Class<?>>();
 
     /**
      * Priority up list
      */
     static {
         APC_PRIORITY_LIST.add(InfrastructureAdvisorAutoProxyCreator.class);
         APC_PRIORITY_LIST.add(AspectJAwareAdvisorAutoProxyCreator.class);
         APC_PRIORITY_LIST.add(AnnotationAwareAspectJAutoProxyCreator.class);
     }

As shown in the figure above, because the first index of the class infrastructureasuggestorautoproxycreator in the list is 0, the requiredpriority is the smallest, and it does not enter, so the beanClassName is not reset, nothing is done, and null is returned
When will the enhanced proxy class be generated?
The class diagram of infrastructure advisor autoproxycreator is as follows:

As shown in the figure above, look at the two core methods: postProcessBeforeInstantiation of the InstantiationAwareBeanPostProcessor interface before instantiation + postProcessAfterInitialization of the BeanPostProcessor interface after initialization

     @Override
     public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
         Object cacheKey = getCacheKey(beanClass, beanName);
 
         if (beanName == null || !this.targetSourcedBeans.contains(beanName)) {
             if (this.advisedBeans.containsKey(cacheKey)) {//If it already exists, return directly
                 return null;
             }//Whether basic components (basic construction does not require agents): Advice, Pointcut, Advisor and AopInfrastructureBean are all basic construction
             if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
                 this.advisedBeans.put(cacheKey, Boolean.FALSE);//Add the adviedbeans concurrenthashmap < k = object, v = Boolean > flag to indicate whether the implementation needs to be enhanced. Here, the basic build beans do not need an agent and are set to false for later use after the instantiation of postProcessAfterInitialization.
                 return null;
             }
         }
 
         // TargetSource is an interface reserved by spring aop for user-defined instantiation. If there is a TargetSource, it will not be instantiated by default, but instantiated in a user-defined way. We do not define it and do not enter it
         if (beanName != null) {
             TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
             if (targetSource != null) {
                 this.targetSourcedBeans.add(beanName);
                 Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
                 Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
                 this.proxyTypes.put(cacheKey, proxy.getClass());
                 return proxy;
             }
         }
 
         return null;
     }

Through tracing, since infrastructure advisor autoproxycreator is an infrastructure class,

advisedBeans.put(cacheKey, Boolean.FALSE)

Add the adviedbeans concurrenthashmap < k = object, v = Boolean > flag to indicate whether the implementation needs to be enhanced. Here, the basic build beans do not need an agent and are set to false for later use after the instantiation of postProcessAfterInitialization

Let's look at the source code of postProcessAfterInitialization as follows:

     @Override
     public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
         if (bean != null) {
             Object cacheKey = getCacheKey(bean.getClass(), beanName);
             if (!this.earlyProxyReferences.contains(cacheKey)) {
                 return wrapIfNecessary(bean, beanName, cacheKey);
             }
         }
         return bean;
     }
 
     protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
       // If it is a user-defined instance, you do not need to enhance the processing and return directly
         if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
             return bean;
         }// Query the map cache, mark false, and return directly without enhancement
         if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
             return bean;
         }// Judge the spring AOP basic construction class, mark it false, and return it directly without enhancement
         if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
             this.advisedBeans.put(cacheKey, Boolean.FALSE);
             return bean;
         }
 
         // Get enhanced list < advisor > Advisors
         Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
       // If there are enhancements
         if (specificInterceptors != DO_NOT_PROXY) {
             this.advisedBeans.put(cacheKey, Boolean.TRUE);// The flag is enhanced to TRUE, indicating that the implementation needs to be enhanced
          // Generate enhanced proxy class
             Object proxy = createProxy(
                     bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
             this.proxyTypes.put(cacheKey, proxy.getClass());
             return proxy;
         }
      // If there is no enhancement, mark false as the cache and enter again to improve efficiency. In line 16, use the cache to verify first
         this.advisedBeans.put(cacheKey, Boolean.FALSE);
         return bean;
     }

The core method createProxy is as follows:

  • ProxyTransactionManagementConfiguration
 @Configuration
 public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {
 
     @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
     @Role(BeanDefinition.ROLE_INFRASTRUCTURE)//Define transaction enhancer
     public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() {
         BeanFactoryTransactionAttributeSourceAdvisor j = new BeanFactoryTransactionAttributeSourceAdvisor();
         advisor.setTransactionAttributeSource(transactionAttributeSource());
         advisor.setAdvice(transactionInterceptor());
         advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
         return advisor;
     }
 
     @Bean
     @Role(BeanDefinition.ROLE_INFRASTRUCTURE)//Define annotation based transaction attribute resources
     public TransactionAttributeSource transactionAttributeSource() {
         return new AnnotationTransactionAttributeSource();
     }
 
     @Bean
     @Role(BeanDefinition.ROLE_INFRASTRUCTURE)//Define transaction interceptors
     public TransactionInterceptor transactionInterceptor() {
         TransactionInterceptor interceptor = new TransactionInterceptor();
         interceptor.setTransactionAttributeSource(transactionAttributeSource());
         if (this.txManager != null) {
             interceptor.setTransactionManager(this.txManager);
         }
         return interceptor;
     }
 
 }

Core method: transactionAdvisor() transaction weaving

An advisor is defined to set transaction properties, transaction interceptor and order. The core is the transaction interceptor transaction interceptor

TransactionInterceptor uses the common spring transaction infrastructure to implement "declarative transactions", inherits from the TransactionAspectSupport class (which contains integration with spring's underlying transaction API) and implements the MethodInterceptor interface. The spring class diagram is as follows

The interception function of the transaction interceptor depends on the implementation of the MethodInterceptor interface. Students familiar with spring must be familiar with the MethodInterceptor. This is the spring method interceptor, mainly based on the invoke method:

 @Override
     public Object invoke(final 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);
 
         // Invoking the invokeWithinTransaction method of TransactionAspectSupport
         return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
             @Override
             public Object proceedWithInvocation() throws Throwable {
                 return invocation.proceed();
             }
         });
     }

As shown in figure TransactionInterceptor, the invoke method of the MethodInterceptor interface is duplicated and the invokeWithinTransaction() method of the parent class TransactionAspectSupport is invoked in the invoke method. The source code is as follows:

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
             throws Throwable {  
 
         // If the transaction attribute is empty, the method is a non transaction (non programmatic transaction)
         final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
         final PlatformTransactionManager tm = determineTransactionManager(txAttr);
         final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
      // Standard declarative transaction: if the transaction attribute is empty or non callback biased transaction manager
         if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
             // Standard transaction demarcation with getTransaction and commit/rollback calls.
             TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
             Object retVal = null;
             try {
                 // Here is a surround enhancement. You can define the enhancement implementation before and after the process
                 // Method execution
                 retVal = invocation.proceedWithInvocation();
             }
             catch (Throwable ex) {
                 // According to the transaction definition, the exception needs to be rolled back, otherwise the transaction is committed
                 completeTransactionAfterThrowing(txInfo, ex);
                 throw ex;
             }
             finally {//Clear the current transaction information and reset it to the old one
                 cleanupTransactionInfo(txInfo);
             }//Commit transactions before returning results
             commitTransactionAfterReturning(txInfo);
             return retVal;
         }
      // Programming transactions: (callback bias)
         else {
             final ThrowableHolder throwableHolder = new ThrowableHolder();
 
             // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
             try {
                 Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr,
                         new TransactionCallback<Object>() {
                             @Override
                             public Object doInTransaction(TransactionStatus status) {
                                 TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
                                 try {
                                     return invocation.proceedWithInvocation();
                                 }
                                 catch (Throwable ex) {// If the exception needs to be rolled back
                                     if (txAttr.rollbackOn(ex)) {
                                         // If it is a runtime exception, return
                                         if (ex instanceof RuntimeException) {
                                             throw (RuntimeException) ex;
                                         }// Throw ThrowableHolderException for other exceptions
                                         else {
                                             throw new ThrowableHolderException(ex);
                                         }
                                     }// If rollback is not required
                                     else {
                                         // Define the exception and eventually commit the transaction directly
                                         throwableHolder.throwable = ex;
                                         return null;
                                     }
                                 }
                                 finally {//Clear the current transaction information and reset it to the old one
                                     cleanupTransactionInfo(txInfo);
                                 }
                             }
                         });
 
                 // Throw up anomaly
                 if (throwableHolder.throwable != null) {
                     throw throwableHolder.throwable;
                 }
                 return result;
             }
             catch (ThrowableHolderException ex) {
                 throw ex.getCause();
             }
             catch (TransactionSystemException ex2) {
                 if (throwableHolder.throwable != null) {
                     logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                     ex2.initApplicationException(throwableHolder.throwable);
                 }
                 throw ex2;
             }
             catch (Throwable ex2) {
                 if (throwableHolder.throwable != null) {
                     logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                 }
                 throw ex2;
             }
         }
     }

As shown in the figure above, we mainly look at the first branch, declarative transaction. The core process is as follows:

  1. createTransactionIfNecessary(): create a transaction if necessary
  2. InvocationCallback's proceedWithInvocation(): InvocationCallback is the internal callback interface of the parent class, which is implemented in the subclass for the parent class to call, and invocationinterceptor in the subclass proceed(). Callback method execution
  3. Exception rollback completeTransactionAfterThrowing()
  • createTransactionIfNecessary():
 protected TransactionInfo createTransactionIfNecessary(
             PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {
 
         // If no name is defined, define the ID of the connection point as the name of the transaction
         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) {
                 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);
     }

The core is:
(1) : gettransaction(), get the TransactionStatus of the transaction according to the transaction attribute. All of them call platformtransactionmanager getTransaction()
(2) : prepareTransactionInfo(), construct a TransactionInfo transaction information object and bind the current thread: ThreadLocal

  • invocation.proceed() callback business method:
    The final implementation class is ReflectiveMethodInvocation. The class diagram is as follows:

    As shown in the figure above, the reflective methodinvocation class implements the ProxyMethodInvocation interface, but ProxyMethodInvocation inherits the three-tier interface... ProxyMethodInvocation - > methodinvocation - > invocation - > joinpoint

Joinpoint: connection point interface, which defines the execution interface: object processed() throws throwable; Execute the current connection point and jump to the next interceptor on the interceptor chain
Invocation: the calling interface, inherited from Joinpoint, defines the interface for obtaining parameters: Object[] getArguments(); It is a connection point with parameters that can be intercepted by interceptors
MethodInvocation: method calling interface, which inherits from Invocation and defines the interface of obtaining method: Method getMethod(); Is an interceptable connection point method with parameters
ProxyMethodInvocation: proxy method calling interface, which inherits from MethodInvocation and defines the interface for obtaining proxy object: Object getProxy(); Is a method call join point method executed by a proxy class
Reflective methodinvocation: implements the ProxyMethodInvocation interface, which naturally implements all interfaces of the parent class interface. Get the proxy class, get the method, get the parameters, use the proxy class to execute this method and automatically jump to the next connection point

Let's take a look at the source code of the proceed method:

 @Override
     public Object proceed() throws Throwable {
         //    When starting, the index is - 1, wake up the connection point, and then increase
         if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
             return invokeJoinpoint();
         }
 
         Object interceptorOrInterceptionAdvice =
                 this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
         if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
             // Dynamic method matching verification is performed here. Static method matching has already been verified (there are two typical methods of MethodMatcher interface: dynamic / static verification)
             InterceptorAndDynamicMethodMatcher dm =
                     (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
             if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
                 return dm.interceptor.invoke(this);
             }
             else {
                 // Dynamic matching failed. Skip the current interception and enter the next one (interceptor chain)
                 return proceed();
             }
         }
         else {
             // It is an interceptor, so we only call it: the pointcut will be statically evaluated before the object is constructed.
             return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
         }
     }

What we finally call here is ((methodinterceptor) interceptorinterceptionadvice) invoke(this); The transaction interceptor is the transaction interceptor callback target business method (addUserBalanceAndUser)

  • completeTransactionAfterThrowing()
    Finally, call the rollback() of AbstractPlatformTransactionManager to commit the transaction commitTransactionAfterReturning() and finally call the commit() of AbstractPlatformTransactionManager

  • Summary:
    It can be seen that whether it is a programmatic transaction or a declarative transaction, the final source code is to call the three methods of the PlatformTransactionManager interface of the transaction manager:

    1. getTransaction
    2. commit
    3. rollback
4.3 transaction core source code

As mentioned above, the PlatformTransactionManager top-level interface defines the most core transaction management methods. The following layer is the AbstractPlatformTransactionManager abstract class, which implements the methods of the PlatformTransactionManager interface and defines some abstract methods for subclass expansion. Finally, the following layer is two classic transaction managers:

  1. Data source transaction manager, namely JDBC single database transaction manager, is implemented based on Connection
  2. JTA transaction manager, that is, multi database transaction manager (also known as distributed transaction manager), implements JTA specification and uses XA protocol for two-stage submission

We only look at the source code of DataSourceTransactionmanager based on JDBC connection

PlatformTransactionManager interface:

public interface PlatformTransactionManager {
    // Get transaction status
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
  // Transaction commit
    void commit(TransactionStatus status) throws TransactionException;
  // Transaction rollback
    void rollback(TransactionStatus status) throws TransactionException;
}
4.3.1 getTransaction get transaction


AbstractPlatformTransactionManager implements getTransaction() method as follows:

     @Override
     public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
         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();
         }
       // If a transaction already exists
         if (isExistingTransaction(transaction)) {
             // Simultaneous interpreting according to different communication mechanisms
             return handleExistingTransaction(definition, transaction, debugEnabled);
         }
 
         // Timeout cannot be less than the default value
         if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
             throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
         }
 
         // There is no transaction at present, and the propagation mechanism = MANDATORY (supports the current transaction, no transaction error is reported), and an error is reported
         if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
             throw new IllegalTransactionStateException(
                     "No existing transaction found for transaction marked with propagation 'mandatory'");
         }// There is no transaction at present, propagation mechanism = REQUIRED/REQUIRED_NEW/NESTED. In these three cases, you need to start a new transaction and add transaction synchronization
         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 {// Whether to start synchronization. / / on / / on
                 boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                 DefaultTransactionStatus status = newTransactionStatus(
                         definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                 doBegin(transaction, definition);// Open new transaction
                 prepareSynchronization(status, definition);//Preparatory synchronization
                 return status;
             }
             catch (RuntimeException ex) {
                 resume(null, suspendedResources);
                 throw ex;
             }
             catch (Error err) {
                 resume(null, suspendedResources);
                 throw err;
             }
         }
         else {
             // There is no transaction at present. There is no transaction at present, and the propagation mechanism = PROPAGATION_SUPPORTS/PROPAGATION_NOT_SUPPORTED/PROPAGATION_NEVER, create an "empty" transaction in these three cases: there is no actual transaction, but it may be synchronous. Warning: the isolation level is defined but there is no real transaction initialization. The isolation level is ignored. There is an isolation level but there is no actual transaction initialization defined. There is an isolation level but there is no actual transaction initialization defined,
             if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
                 logger.warn("Custom isolation level specified but no actual transaction initiated; " +
                         "isolation level will effectively be ignored: " + definition);
             }
             boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
             return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
         }
     }

As shown in the figure above, the source code is divided into two processing lines:

  1. There are already transactions: isExistingTransaction() determines whether there is a transaction and there is transaction handleExistingTransaction (simultaneous interpreting) based on different propagation mechanisms.
  2. No transactions exist at present: simultaneous interpreting of different communication mechanisms.

The source code of handleExistingTransaction() is as follows:

     private TransactionStatus handleExistingTransaction(
             TransactionDefinition definition, Object transaction, boolean debugEnabled)
             throws TransactionException {
      // 1.NERVER (does not support the current transaction; if the current transaction exists, throw an exception) reports an error
         if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
             throw new IllegalTransactionStateException(
                     "Existing transaction found for transaction marked with propagation 'never'");
         }
       // 2.NOT_SUPPORTED (current transaction is not supported, existing synchronization will be suspended) suspends the current transaction
         if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
             if (debugEnabled) {
                 logger.debug("Suspending current transaction");
             }
             Object suspendedResources = suspend(transaction);
             boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
             return prepareTransactionStatus(
                     definition, null, false, newSynchronization, debugEnabled, suspendedResources);
         }
      // 3.REQUIRES_NEW suspends the current transaction and creates a new transaction
         if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
             if (debugEnabled) {
                 logger.debug("Suspending current transaction, creating new transaction with name [" +
                         definition.getName() + "]");
             }// Suspend current transaction
            SuspendedResourcesHolder suspendedResources = suspend(transaction);
             try {// Create a new transaction
                 boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                 DefaultTransactionStatus status = newTransactionStatus(
                         definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                 doBegin(transaction, definition);
                 prepareSynchronization(status, definition);
                 return status;
             }
             catch (RuntimeException beginEx) {
                 resumeAfterBeginException(transaction, suspendedResources, beginEx);
                 throw beginEx;
             }
             catch (Error beginErr) {
                 resumeAfterBeginException(transaction, suspendedResources, beginErr);
                 throw beginErr;
             }
         }
      // 4.NESTED transactions
         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() + "]");
             }// Whether savepoints are supported: non JTA transactions take this branch. AbstractPlatformTransactionManager defaults to true. Jtatatransactionmanager replicates the method. false. DataSourceTransactionmanager does not replicate it. It is still true,
             if (useSavepointForNestedTransaction()) { 
                 // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.
                 DefaultTransactionStatus status =
                         prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
                 status.createAndHoldSavepoint();// Create savepoint
                 return status;
             }
             else {
                 // JTA transaction takes this branch and creates a new transaction
                 boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                 DefaultTransactionStatus status = newTransactionStatus(
                         definition, transaction, true, newSynchronization, debugEnabled, null);
                 doBegin(transaction, definition);
                 prepareSynchronization(status, definition);
                 return status;
             }
         }
 
         
         if (debugEnabled) {
             logger.debug("Participating in existing transaction");
         }
         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");
                 }
             }
         }// Here promotion_ Supports or promotion_ Required or promotion_ Mandatory: if a transaction exists, just join the transaction. The third parameter of prepareTransactionStatus is whether a new transaction is required. false means no new things are needed
         boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
         return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
     }

As shown in the figure above, when the current thread already has a transaction, the new processing of different isolation levels:

  1. NERVER: current transaction is not supported; If the current transaction exists, an exception is thrown: "Existing transaction found for transaction marked with propagation 'never'"
  2. NOT_SUPPORTED: current transaction is not supported, existing synchronization will be suspended: suspend()
  3. REQUIRES_NEW suspends the current transaction and creates a new transaction:
    1. suspend()
    2. doBegin()
  4. NESTED transactions
    1. Non JTA transaction: createAndHoldSavepoint() creates JDBC 3 0 savepoint, no synchronization required
    2. JTA transaction: start a new transaction. Dobegin() + preparessynchronization() needs to be synchronized

There are several core methods: suspend the current transaction () and start a new transaction doBegin()

The source code of suspend() is as follows:

 protected final SuspendedResourcesHolder suspend(Object transaction) throws TransactionException {
         if (TransactionSynchronizationManager.isSynchronizationActive()) {// 1. Synchronization currently exists,
             List<TransactionSynchronization> suspendedSynchronizations = doSuspendSynchronization();
             try {
                 Object suspendedResources = null;
                 if (transaction != null) {// Transaction is not empty, suspend transaction
                     suspendedResources = doSuspend(transaction);
                 }// Unbind various attributes of the current transaction: name, read-only, isolation level, and whether it is a real transaction
                 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 ex) {
                 // doSuspend failed - original transaction is still active...
                 doResumeSynchronization(suspendedSynchronizations);
                 throw ex;
             }
             catch (Error err) {
                 // doSuspend failed - original transaction is still active...
                 doResumeSynchronization(suspendedSynchronizations);
                 throw err;
             }
         }// 2. There is no synchronization, but the transaction is not empty. Suspend the transaction
         else if (transaction != null) {
             // Transaction active but no synchronization active.
             Object suspendedResources = doSuspend(transaction);
             return new SuspendedResourcesHolder(suspendedResources);
         }// 2. There is no synchronization, but the transaction is empty and nothing needs to be done
         else {
             // Neither transaction nor synchronization active.
             return null;
         }
     }

Dospend() suspends the transaction. The AbstractPlatformTransactionManager abstract class dospend() will report an error: suspension is not supported. If the specific transaction executor supports it, copy dospend(). The implementation of DataSourceTransactionManager is as follows:

     @Override
     protected Object doSuspend(Object transaction) {
         DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
         txObject.setConnectionHolder(null);
         return TransactionSynchronizationManager.unbindResource(this.dataSource);
     }

The core operations of suspending DataSourceTransactionManager transactions are:

  1. Clear the connectionHolder database connection holder of the current transaction
  2. Unbind datasource from current thread. In fact, ThreadLocal removes the corresponding variable (private static final ThreadLocal < map < object, Object > > resources = new namedthreadlocal < map < object, Object > > ("Transactional resources")

TransactionSynchronizationManager transaction synchronization manager. This class maintains multiple thread local variables ThreadLocal, as shown in the following figure:

 public abstract class TransactionSynchronizationManager {
 
     private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
     // Transaction resources: Map < K, V > two data pairs. 1. Session factory and session k=SqlsessionFactory v=SqlSessionHolder 2 Data source and connection k=DataSource v=ConnectionHolder
     private static final ThreadLocal<Map<Object, Object>> resources =
             new NamedThreadLocal<Map<Object, Object>>("Transactional resources");
     // Transaction synchronization
     private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
             new NamedThreadLocal<Set<TransactionSynchronization>>("Transaction synchronizations");
   // Current transaction name
     private static final ThreadLocal<String> currentTransactionName =
             new NamedThreadLocal<String>("Current transaction name");
   // Read only attribute of the current transaction
     private static final ThreadLocal<Boolean> currentTransactionReadOnly =
             new NamedThreadLocal<Boolean>("Current transaction read-only status");
   // Isolation level of the current transaction
     private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
             new NamedThreadLocal<Integer>("Current transaction isolation level");
   // Is there a transaction
     private static final ThreadLocal<Boolean> actualTransactionActive =
             new NamedThreadLocal<Boolean>("Actual transaction active");
 . . . 
 }

The source code of doBegin() is as follows:

     @Override
     protected void doBegin(Object transaction, TransactionDefinition definition) {
         DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
         Connection con = null;
 
         try {// If the transaction has no connection or the connection is in the transaction synchronization state, reset the new connectionHolder
             if (!txObject.hasConnectionHolder() ||
                     txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
                 Connection newCon = this.dataSource.getConnection();
                 if (logger.isDebugEnabled()) {
                     logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
                 }// Reset new connectionHolder
                 txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
             }
       //Set the new connection as in transaction synchronization
             txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
             con = txObject.getConnectionHolder().getConnection();
          //conn set transaction isolation level, read-only
             Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
             txObject.setPreviousIsolationLevel(previousIsolationLevel);//Set transaction isolation level for DataSourceTransactionObject
 
             // If it is automatic submission, switch to manual submission
             // 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);
             }
       // If read-only, execute sql to set the transaction read-only
             prepareTransactionalConnection(con, definition);
             txObject.getConnectionHolder().setTransactionActive(true);// Set the transaction on state of the connection holder
 
             int timeout = determineTimeout(definition);
             if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
                 txObject.getConnectionHolder().setTimeoutInSeconds(timeout);// Set timeout seconds
             }
 
             // Bind the connection holder to the current thread
             if (txObject.isNewConnectionHolder()) {
                 TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
             }
         }
 
         catch (Throwable ex) {
             if (txObject.isNewConnectionHolder()) {
                 DataSourceUtils.releaseConnection(con, this.dataSource);
                 txObject.setConnectionHolder(null, false);
             }
             throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
         }
     }

As shown in the figure above, the core operations of doBegin() are:

  1. DataSourceTransactionObject "data source transaction object", set ConnectionHolder, and then set various properties for ConnectionHolder: Auto commit, timeout, transaction on, isolation level
  2. Bind a thread local variable to the current thread, key=DataSource data source, v=ConnectionHolder database connection
4.3.2 commit transaction

Before explaining the source code, take a look at the resource management class:

SqlSessionSynchronization is an internal class of SqlSessionUtils, which inherits from the TransactionSynchronizationAdapter abstract class and implements the transaction synchronization interface TransactionSynchronization
The class diagram is as follows:

The TransactionSynchronization interface defines the corresponding resource (JDBC transaction, then SqlSessionSynchronization) management method during transaction operation:

     // Pending transactions
   void suspend();
     
     // Wake up transaction
     void resume();
     
     void flush();
 
     // Before committing a transaction
     void beforeCommit(boolean readOnly);
 
     // Before the commit transaction is completed
     void beforeCompletion();
 
     // After committing a transaction
     void afterCommit();
 
     // After the commit transaction is completed
     void afterCompletion(int status);

Many of them use these interfaces to manage transactions

Commit commit transaction

The commit source code of AbstractPlatformTransactionManager is as follows:

     @Override
     public final void commit(TransactionStatus status) throws TransactionException {
         if (status.isCompleted()) {// If the transaction is completed, an error is reported and cannot be committed again
             throw new IllegalTransactionStateException(
                     "Transaction is already completed - do not call commit or rollback more than once per transaction");
         }
 
         DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
         if (defStatus.isLocalRollbackOnly()) {// If the transaction is explicitly marked for rollback,
             if (defStatus.isDebug()) {
                 logger.debug("Transactional code has requested rollback");
             }
             processRollback(defStatus);//Execute rollback
             return;
         }//If global rollback is not required, commit and global rollback
         if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
             if (defStatus.isDebug()) {
                 logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
             }//Execute rollback
             processRollback(defStatus);
             // Throw an "unexpected rollback exception" only at the outermost transaction boundary (new transaction) or when explicitly requested
             if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
                 throw new UnexpectedRollbackException(
                         "Transaction rolled back because it has been marked as rollback-only");
             }
             return;
         }
      // Execute commit transaction
         processCommit(defStatus);
     }

As shown in the figure above, various judgments:

  1. If the transaction is explicitly marked as local rollback, rollback is performed
  2. If the global rollback is committed when the global rollback is not required and the global rollback is performed, the rollback is performed
  3. Commit a transaction. The core method is processCommit()

processCommit is as follows:

 private void processCommit(DefaultTransactionStatus status) throws TransactionException {
         try {
             boolean beforeCompletionInvoked = false;
             try {//3 pre operations
                 prepareForCommit(status);
                 triggerBeforeCommit(status);
                 triggerBeforeCompletion(status);
                 beforeCompletionInvoked = true;//3 preceding operations called
                 boolean globalRollbackOnly = false;//New transaction or global rollback failed
                 if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
                     globalRollbackOnly = status.isGlobalRollbackOnly();
                 }//1. There are savepoints, that is, nested transactions
                 if (status.hasSavepoint()) {
                     if (status.isDebug()) {
                         logger.debug("Releasing transaction savepoint");
                     }//Release savepoint
                     status.releaseHeldSavepoint();
                 }//2. New services
                 else if (status.isNewTransaction()) {
                     if (status.isDebug()) {
                         logger.debug("Initiating transaction commit");
                     }//Call the transaction processor to commit the transaction
                     doCommit(status);
                 }
                 // 3. If the transaction is not a new transaction and the global rollback fails, but no exception is obtained when committing, an exception is thrown
                 if (globalRollbackOnly) {
                     throw new UnexpectedRollbackException(
                             "Transaction silently rolled back because it has been marked as rollback-only");
                 }
             }
             catch (UnexpectedRollbackException ex) {
                 // After triggering, the transaction is synchronized and the status is rollback
                 triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
                 throw ex;
             }// Transaction exception
             catch (TransactionException ex) {
                 // Commit failed rollback
                 if (isRollbackOnCommitFailure()) {
                     doRollbackOnCommitException(status, ex);
                 }// After triggering the callback, the transaction synchronization status is unknown
                 else {
                     triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
                 }
                 throw ex;
             }// Runtime exception
             catch (RuntimeException ex) {
            // If the three preceding steps are not completed, call the last preceding step
                 if (!beforeCompletionInvoked) {
                     triggerBeforeCompletion(status);
                 }// Commit exception rollback
                 doRollbackOnCommitException(status, ex);
                 throw ex;
             }// Other anomalies
             catch (Error err) {  
            // If the three preceding steps are not completed, call the last preceding step
                 if (!beforeCompletionInvoked) {
                     triggerBeforeCompletion(status);
                 }// Commit exception rollback
                 doRollbackOnCommitException(status, err);
                 throw err;
             }
 
             // Trigger afterCommit callbacks, with an exception thrown there
             // propagated to callers but the transaction still considered as committed.
             try {
                 triggerAfterCommit(status);
             }
             finally {
                 triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
             }
 
         }
         finally {
             cleanupAfterCompletion(status);
         }
     }

As shown in the figure above, there are 6 core operations during commit transaction, including 3 pre operations and 3 post operations, as follows:

  1. Prepare for commit (status): the source code is empty and has no expansion

  2. Trigger before commit (status): trigger operation before commit

    protected final void triggerBeforeCommit(DefaultTransactionStatus status) {
        if (status.isNewSynchronization()) {
            if (status.isDebug()) {
                logger.trace("Triggering beforeCommit synchronization");
            }
            TransactionSynchronizationUtils.triggerBeforeCommit(status.isReadOnly());
        }
    }

The source code of triggerBeforeCommit is as follows:

     public static void triggerBeforeCommit(boolean readOnly) {
         for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) {
             synchronization.beforeCommit(readOnly);
         }
     }

As shown in the figure above, the TransactionSynchronizationManager class defines multiple ThreadLocal (thread local variables), one of which is used to save the transaction synchronization of the current thread:

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

Traverse the transaction synchronizer and execute the "before commit" operation for each transaction synchronizer. For example, the jdbc transaction we use will eventually be sqlsessionutils beforeCommit()->this. holder. getSqlSession(). commit(); Submit session. (because the source code is a spring management practice, transaction submission will not be executed in the end, for example, DefaultSqlSession: clear the cache and reset the state)

  1. Trigger before completion (status): trigger the operation before completion. If it is a jdbc transaction, it will be,

SqlSessionUtils. beforeCompletion -> TransactionSynchronizationManager. unbindResource(sessionFactory); Unbind the session factory of the current thread

this.holder.getSqlSession().close(); Close the session. (because the source code is a spring management practice, the transaction close operation will not be performed in the end, such as DefaultSqlSession, and various clearing and closing operations will also be performed)

  1. Trigger after commit (status): trigger the operation after the transaction is committed
    TransactionSynchronizationUtils. triggerAfterCommit() -> TransactionSynchronizationUtils. Invokeaftercommit, as follows:
 public static void invokeAfterCommit(List<TransactionSynchronization> synchronizations) {
         if (synchronizations != null) {
             for (TransactionSynchronization synchronization : synchronizations) {
                 synchronization.afterCommit();
             }
         }
     }

Well, after a quick search, it was copied in the TransactionSynchronizationAdapter and was empty... SqlSessionSynchronization inherited the TransactionSynchronizationAdapter but did not copy this method

  1. triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);

TransactionSynchronizationUtils.TransactionSynchronizationUtils.invokeAfterCompletion, as follows:

     public static void invokeAfterCompletion(List<TransactionSynchronization> synchronizations, int completionStatus) {
         if (synchronizations != null) {
             for (TransactionSynchronization synchronization : synchronizations) {
                 try {
                     synchronization.afterCompletion(completionStatus);
                 }
                 catch (Throwable tsex) {
                     logger.error("TransactionSynchronization.afterCompletion threw exception", tsex);
                 }
             }
         }
     }

After completion: for JDBC transactions, the final:
1. If the session is still alive, close the session
2. Reset various attributes: referenceCount reference count of the SQL session holder of the SQL session synchronizer, synchronizedWithTransaction synchronization transaction, rollbackOnly rollback, deadline timeout point

  1. cleanupAfterCompletion(status);
    1. Set transaction status to completed
    2. If it is a new transaction synchronization, unbind the database resources bound by the current thread and reset the database connection
    3. If there is a pending transaction (nested transaction), wake up various resources of the pending old transaction: database resources and synchronizer
     private void cleanupAfterCompletion(DefaultTransactionStatus status) {
         status.setCompleted();//Set transaction status complete
     //If it is a new synchronization, clear all thread local variables bound by the current thread except resources: including transaction synchronizer, transaction name, read-only attribute, isolation level and real transaction activation status
         if (status.isNewSynchronization()) {
             TransactionSynchronizationManager.clear();
         }//If it is a new transaction synchronization
         if (status.isNewTransaction()) {
             doCleanupAfterCompletion(status.getTransaction());
         }//If there are pending resources
         if (status.getSuspendedResources() != null) {
             if (status.isDebug()) {
                 logger.debug("Resuming suspended transaction after completion of inner transaction");
             }//Wake up the suspended transactions and resources (rebind the previously suspended database resources, wake up the synchronizer, and register the synchronizer with the transaction synchronization manager)
             resume(status.getTransaction(), (SuspendedResourcesHolder) status.getSuspendedResources());
         }
     }

For DataSourceTransactionManager, the source code of doCleanupAfterCompletion is as follows:

     protected void doCleanupAfterCompletion(Object transaction) {
         DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
 
         // If it is the latest connection holder, unbind the < database resource, connectionholder > bound by the current thread
         if (txObject.isNewConnectionHolder()) {
             TransactionSynchronizationManager.unbindResource(this.dataSource);
         }
 
         // Reset database connection (isolation level, read-only)
         Connection con = txObject.getConnectionHolder().getConnection();
         try {
             if (txObject.isMustRestoreAutoCommit()) {
                 con.setAutoCommit(true);
             }
             DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
         }
         catch (Throwable ex) {
             logger.debug("Could not reset JDBC Connection after transaction", ex);
         }
 
         if (txObject.isNewConnectionHolder()) {
             if (logger.isDebugEnabled()) {
                 logger.debug("Releasing JDBC Connection [" + con + "] after transaction");
             }// Resource reference count - 1, close database connection
             DataSourceUtils.releaseConnection(con, this.dataSource);
         }
         // Reset all properties of the connection holder
         txObject.getConnectionHolder().clear();
     }
4.3.3 rollback rollback transaction


The source code of rollback in AbstractPlatformTransactionManager is as follows:

     public final void rollback(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;
         processRollback(defStatus);
     }

The source code of processRollback is as follows:

     private void processRollback(DefaultTransactionStatus status) {
         try {
             try {// Unbind the session factory bound by the current thread and close the session
                 triggerBeforeCompletion(status);
                 if (status.hasSavepoint()) {// 1. If there is a savepoint, it is a nested transaction
                     if (status.isDebug()) {
                         logger.debug("Rolling back transaction to savepoint");
                     }//Rollback to savepoint
                     status.rollbackToHeldSavepoint();
                 }//2. If it is a simple transaction
                 else if (status.isNewTransaction()) {
                     if (status.isDebug()) {
                         logger.debug("Initiating transaction rollback");
                     }//Rollback core method
                     doRollback(status);
                 }//3. There is a current transaction and there is no savepoint, that is, to join the current transaction
                 else if (status.hasTransaction()) {//If it has been marked for rollback or if the joining transaction fails, the global rollback (default true)
                     if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                         if (status.isDebug()) {//When debug ging, it will print: failed to join the transaction - mark the existing transaction as rollback
                             logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                         }//Set the current connectionHolder: roll back when joining an existing transaction
                         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");
                 }
             }
             catch (RuntimeException ex) {//Close the session and reset the SqlSessionHolder property
                 triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
                 throw ex;
             }
             catch (Error err) {
                 triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
                 throw err;
             }
             triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
         }
         finally {
             // Unbind the current thread
             cleanupAfterCompletion(status);
         }
     }

As shown in the figure above, there are several common methods consistent with the transaction submission, so they will not be repeated

Here we mainly look at doRollback. The source code of doRollback() of DataSourceTransactionManager is as follows:

     protected void doRollback(DefaultTransactionStatus status) {
         DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
         Connection con = txObject.getConnectionHolder().getConnection();
         if (status.isDebug()) {
             logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
         }
         try {
             con.rollback();
         }
         catch (SQLException ex) {
             throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
         }
     }

Well, it's not complicated at all. It's the rollback of Connection

4.3.4 sequence diagram

Specially sorted out the sequence diagram (simple new transactions, no savepoints, etc.) as follows:

5 test verification
5.1 environment
5.1.1 environment business simulation

Simulate the user to transfer money to the bank, and user A transfers money to user B. It is necessary to ensure that user A deducts money and user B adds money and rolls back success or failure at the same time

5.1.2 environmental preparation

Test environment: mysql8+mac, mysql8 used in the test (different from the statement for setting transaction variables in mysql5.6, don't care too much)

Test preparation: create a database test and a table user_balance user balance table. id, primary key, name, name, balance, account balance

 mysql> create database test;
 Query OK, 1 row affected (0.05 sec)
 
 mysql> use test;
 Database changed
 mysql> CREATE TABLE `user_balance` (
     ->   `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID Primary key',
     ->   `name` varchar(20) DEFAULT NULL COMMENT 'full name',
     ->   `balance` decimal(10,0) DEFAULT NULL COMMENT 'Account balance',
     ->   PRIMARY KEY (`id`)
     -> ) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8;
 Query OK, 0 rows affected, 1 warning (0.15 sec)
 
// Initialization data: 1000 yuan for both accounts:
mysql> INSERT INTO `user_balance` VALUES ('1', 'Zhang San', '1000'), ('2', 'Li Si', '1000');
Query OK, 2 rows affected (0.06 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> select * from user_balance;                                              
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | Zhang San   |    1000 |
|  2 | Li Si   |    1000 |
+----+--------+---------+
2 rows in set (0.00 sec)
5.2 isolation level measurement
5.2.1 isolation level measurement

General statements:

  1. Start / commit transaction: Start: begin/start transaction is OK, commit: commit;
  2. Query transaction level: select @@transaction_isolation;
  3. Modify transaction level: set global transaction_isolation=‘read-uncommitted’;

Note: after modification, exit and reconnect mysql(mysql -uroot) to take effect (this is a simulation of MySQL 5.6, and MySQL 8 has a directly effective statement)

The following four tests are all conducted after setting the transaction isolation level. The following tests will not be shown

5.2.2 Read Uncommitted

Test steps:

  1. Open two sessions to connect to mysql. Session 1 starts transaction A and session 2 starts transaction B
  2. Execute update in transaction A to make Zhang San's balance 1000-100 = 900, and the query result of transaction A is 900
  3. At this time, transaction A is not committed, and the query result of transaction B is 900, that is, the uncommitted content (the latest version number data read by MVCC snapshot) is read

As shown in the following figure (Session 1 transaction A on the left and session 2 transaction B on the right)

Conclusion: obviously not, because the internal processing data of transaction A is not necessarily the last data. It is likely that transaction A will add 1000 later, and the data read by transaction B is obviously wrong, that is, dirty reading!

5.2.3 Read Committed

Test steps:

  1. Open two sessions to connect to mysql. Session 1 starts transaction A and session 2 starts transaction B
  2. Execute update in transaction A to make Zhang San's balance 1000-100 = 900, and the query result of transaction A is 900. As long as transaction A is not committed, the query data of transaction B remains unchanged or 1000
  3. Transaction A commits, and transaction B query immediately becomes 900, that is, read committed

As shown in the following figure (Session 1 transaction A on the left and session 2 transaction B on the right):

Summary: the problem of dirty reading is solved, but transaction B has not been committed at this time, that is, the same sql data queried multiple times in a transaction is inconsistent, that is, it can not be read repeatedly!

5.2.4 Repeatable Read

Test steps:

  1. Open two sessions to connect to mysql. Session 1 starts transaction A and session 2 starts transaction B
  2. Execute update in transaction A to make Zhang San's balance 1000-100 = 900, and the query result of transaction A is 900. Transaction A is committed, and transaction B's query data remains unchanged
  3. Session 1 starts another transaction C, inserts a piece of "king five" data and submits it. Transaction B still queries two pieces of data, and the data is consistent with the first query, that is, read submitted + repeatable read
  4. Transaction B in session 2 also inserts a piece of data with the same ID, and an error is reported: there is already data with the same ID=3, and the insertion fails! That is, fantasy reading appears

As shown below:

Solutions supported by mysql:

To prevent unreal reading, you can add A range to for update in transaction A, which will eventually generate A gap lock to block other transactions from inserting data, and transaction B can insert successfully immediately after transaction A is committed

5.2.5 Serializable

Test steps:

  1. Open two sessions to connect to mysql. Session 1 starts transaction A and session 2 starts transaction B
  2. Transaction A queries the record with id=2, and transaction B updates the record with id=2. The update operation is blocked until it times out (transaction B update can be executed immediately after transaction A is committed)

As shown in the following figure (Session 1 transaction A on the left and session 2 transaction B on the right):

Conclusion: at the Serializable level, reading is also locked! If it is a row lock (query a row), subsequent modifications to this row will directly block and wait for the first transaction to complete. If it is a table lock (query the whole table), all subsequent modifications to this table will be blocked and wait. It can be seen that only one query locks the corresponding query data, and the performance is really not flattering

5.3 measurement of communication mechanism
5.3.1 test preparation

Environment: spring4+mybatis+mysql+slf4j+logback. Note: the log logback should be configured: the log print is at the debug level, so that the transaction process can be seen
Test code:

  // Test base class: BaseTest
  @Slf4j
  @RunWith(SpringRunner.class)
  @SpringBootTest(classes = StudyDemoApplication.class)
  public class BaseTest {

  }

  // Test subclass: UserBalanceTest
  /**
   * @Description User balance test (transaction)
   * @author denny
   * @date 2018/9/4 11:38 am
   */ 
  public class UserBalanceTest extends BaseTest{
 
     @Resource
     private UserBalanceService userBalanceService;
 
     @Test
     public void testAddUserBalanceAndUser(){
         userBalanceService.addUserBalanceAndUser("Zhao Liu",new BigDecimal(1000));
     }
     
     public static void main(String[] args) {
         
     }
     
  }

  // UserBalanceImpl
  @Slf4j
  @Service
  public class UserBalanceImpl implements UserBalanceService {
 
      @Resource
     private UserService userService;
     @Resource
     private UserBalanceRepository userBalanceRepository;
 
     /**
      * Create user
      *
      * @param userBalance
      * @return
      */
     @Override
     public void addUserBalance(UserBalance userBalance) {
         this.userBalanceRepository.insert(userBalance);
     }
 
     /**
      * Create user and create account balance
      *
      * @param name
      * @param balance
      * @return
      */
     @Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class)
     @Override
     public void addUserBalanceAndUser(String name, BigDecimal balance) {
         log.info("[addUserBalanceAndUser] begin!!!");
         //1. New users
         userService.addUser(name);
         //2. New user balance
         UserBalance userBalance = new UserBalance();
         userBalance.setName(name);
         userBalance.setBalance(new BigDecimal(1000));
         this.addUserBalance(userBalance);
         log.info("[addUserBalanceAndUser] end!!!");
     }
  }

As shown in the figure above:

addUserBalanceAndUser(){
  addUser(name);//Add user
  addUserBalance(userBalance);//Add user balance
}

addUserBalanceAndUser starts a transaction, and the internal method addUser also declares the transaction, as follows:

UserServiceImpl:

  @Slf4j
  @Service
  public class UserServiceImpl implements UserService{
     @Resource
     private UserRepository userRepository;
 
     @Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class)
     @Override
     public void addUser(String name) {
         log.info("[addUser] begin!!!");
         User user = new User();
         user.setName(name);
         userRepository.insert(user);
         log.info("[addUser] end!!!");
     }
  }
5.3.2 actual measurement
5.3.2.1 REQUIRED

If there is no current transaction, create a new transaction. If there is a current transaction, join the transaction. Default selection

External methods and internal methods are REQUIRED:

As shown in the above figure: the external method starts the transaction. Since there is no transaction, Registering registers a new transaction; The internal method Fetched obtains the existing transaction and uses it, which is in line with the expectation

5.3.2.2 SUPPORTS

The current transaction is supported. If there is a transaction, the transaction will be joined. If there is no transaction, the transaction will be executed as a non transaction
External method required, internal SUPPORTS

As shown in the figure above, the external method creates a transaction, and the propagation mechanism is required. The internal method Participating in existing transaction joins the existing external transaction and finally submits the transaction together, which meets the expectation

5.3.2.3 MANDATORY

Support the current transaction. If there is a transaction, join the transaction. If there is no transaction, throw an exception

There is no external transaction, internal MANDATORY:

As shown in the figure above, there are no external transactions and internal MANDATORY errors, which is in line with expectations

5.3.2.4 REQUIRES_NEW

Create a new transaction. If there is a current transaction, suspend the current transaction. After the new transaction is completed, continue to execute the old transaction

External method REQUIRED, internal method REQUIRED_ NEW

As shown in the figure above, the external method REQUIRED creates a new transaction, and the internal method REQUIRES_NEW suspends the old transaction and creates a new transaction. After the new transaction is completed, wake up the old transaction and continue to execute. Meet expectations

5.3.2.5 NOT_SUPPORTED

Perform operations in a non transactional manner. If there is a transaction currently, suspend the current transaction

External method REQUIRED, internal method NOT_SUPPORTED

As shown in the figure above, the external method creates transaction A, the internal method does not support transaction, suspends transaction A, the internal method completes execution, and wakes up transaction A to continue execution. Meet expectations

5.3.2.6 NEVER

NEVER: execute in a non transactional manner. If there is a transaction currently, an exception will be thrown.

External method REQUIRED, internal method NEVER:

As shown in the figure above, the external method REQUIRED creates a transaction, and the internal method NEVER reports an error if there is a transaction, which is expected

5.3.2.7 NESTED

If a transaction currently exists, it is executed within a nested transaction. If there is currently no transaction, perform an operation similar to REQUIRED

Internal method

As shown in the figure above, the external method REQUIRED creates a transaction, and the internal method NESTED constructs an embedded transaction and creates a savepoint. After the internal transaction runs, release the savepoint and continue to execute the external transaction. Finally, commit together with external transactions. In the figure above, there is only one sqlSession object, and it is also one during commit. Meet expectations

Note: NESTED and requirements_ New difference?

  1. Rollback: NESTED creates a savepoint before creating the inner transaction. The rollback of the inner transaction only rolls back to the savepoint and will not affect the outer transaction. If the outer transaction is rolled back, it will be rolled back together with the inner transaction; REQUIRES_NEW constructs a new transaction and the outer transaction are two independent transactions that do not affect each other
  2. Commit: NESTED is a NESTED transaction, which is a sub transaction of an outer transaction. If the external transaction is committed, the internal transaction is submitted together, and there is only one commit; REQUIRES_NEW is a new transaction. It is a completely independent transaction. It is committed twice independently

Strongly note:
NESTED transactions can be rolled back to the savepoint by themselves, but if the thrown up exception in the NESTED transaction method can also be caught by the external method, the external transaction will be rolled back. Therefore, if it is expected that the rollback of the internal NESTED exception will not affect the external transaction, the exception of the NESTED transaction needs to be caught. As follows:

 @Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class)
    @Override
    public void addUserBalanceAndUser(String name, BigDecimal balance) {
         log.info("[addUserBalanceAndUser] begin!!!");
         //1. The new user balance -- "will eventually be inserted successfully and will not be affected by nested rollback exceptions
         UserBalance userBalance = new UserBalance();
         userBalance.setName(name);
         userBalance.setBalance(new BigDecimal(1000));
         this.addUserBalance(userBalance);
         //2. For new users, exceptions of nested transactions are captured here and cannot be obtained by external transactions. Otherwise, external transactions will be rolled back!
         try{
             // Nested transaction @ Transactional(propagation= Propagation.NESTED, rollbackFor = Exception.class) -- the exception will be rolled back to the savepoint
             userService.addUser(name);
         }catch (Exception e){
             // Here you can add your own business according to the actual situation!
             log.error("Nested transaction[ addUser]Abnormal!",e);
         }
 
         log.info("[addUserBalanceAndUser] end!!!");
     }

Topics: Java Database Spring