Running with Spring Boot v2.5.7, MySQL 8.0

Spring Transaction is a high-level abstraction of the JDBC API for Transaction management, which supports two modes of Transaction management: Declarative Transaction Management and Programmatic Transaction Management Declarative transaction management is hosted by the @Transactional annotation, while programmatic transaction management is implemented by Either TransactionManager or TransactionTemplate (the latter is recommended). Annotation-based declarative transaction management is concise and elegant, and can effectively converge the logic of crosscutting concerns, but there are pitfalls behind extreme simplicity, such as large transactions, transactions that do not roll back properly, etc. Programmatic transaction management is more flexible in controlling transaction granularity than declarative transaction management, which is often necessary!

This article will focus on the source level for you to interpret the implementation principle of declarative transaction management, so as to help you in the work of a better, more accurate use of declarative transaction management.

Writing in the front

In a database system, a transaction usually consists of a set of data manipulation statements (DML) and query statements, considered as atomic units, all of which are either committed or rolled back. Database transactions generally follow the ACID principle, which splices the first letters of Atomicity, Consistency, Isolation, and persistence respectively. In common terms: 1) atomicity, all data manipulation statements within a transaction are either committed or rolled back; 2) Consistency: after the execution of a transaction, the data in the database is either in the state before or after the transaction; 3) Isolation. In a concurrent environment, when multiple different transactions operate the same data at the same time, each transaction has its own complete data space; 4) Persistence: after a transaction is successfully submitted, the changes of relevant data in the database must be permanently saved. Even if the database system is restarted due to a crash, the database can still be restored to the state after the transaction is successfully submitted.

Isolation needs to be talked about separately in the ACID rule! Dirty read, non-repeatable read, and phantom read problems can occur when multiple different transactions are executed simultaneously. To solve these problems, the concept of isolation level comes along: isolation level is a measure of transaction isolation. In SQL-92 standard, four isolation levels are clearly defined: READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, and SERIALIZABLE. Of the four isolation levels, READ_UNCOMMITTED isolates transactions at the lowest level, or even at all; SERIALIZABLE has the highest degree of transaction isolation, preventing dirty and unrepeatable reads as well as phantom reads. Note: The choice of isolation level often requires a balance between performance and reliability and consistency.

| Isolation Level | Dirty Reads | Non-repeatable Reads | Phantom Reads | | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | | READ_UNCOMMITTED | | | |))) | READ_COMMITTED) | | x | |)) | REPEATABLE_READ | * | |) | | the SERIALIZABLE | * * | | |Copy the code

Dirty read refers to reading a row in one transaction that has not been committed in another transaction. Non-repeatable read means that the same row is read multiple times in the same transaction, but the contents are different each time. Phantom reading refers to the reading of multiple rows under the same condition in the same transaction, but each reading is either more or less than the previous reading, as if it is an illusion. Dirty read

| Time Series | Transaction A | Transaction B | |-------------|--------------------------------------------|------------------------------------| | T1 | UPDATE user SET  name = 'java' WHERE id = 1 | | | T2 | | SELECT name FROM user WHERE id = 1 | | T3 | ROLLBACK | |Copy the code

Assume the current isolation level is READ_UNCOMMITTED. In transaction A, the user name with ID 1 is updated; Although transaction A has not committed, the updated user name is still read from transaction B; If transaction A rolls back, the user name read by transaction B disappears and does not exist in the user table at all! Unrepeatable read

| Time Series | Transaction A                      | Transaction B                              |
|-------------|------------------------------------|--------------------------------------------|
| T1          | SELECT name FROM user WHERE id = 1 |                                            |
| T2          |                                    | UPDATE user SET name = 'java' WHERE id = 1 |
| T3          |                                    | COMMIT                                     |
| T4          | SELECT name FROM user WHERE id = 1 |                                            |
Copy the code

Assume the current isolation level is READ_COMMITTED. First, in transaction A, read the user name with ID 1; Then update the user’s username in transaction B and commit the change; Then, in transaction A, the user name is read again, but this time it is A different one. Phantom read

| Time Series | Transaction A | Transaction B | |-------------|------------------------------------|------------------------------------------------| | T1 | SELECT name  FROM user WHERE id < 5 | | | T2 | | INSERT INTO user(id, name) VALUE (4, 'golang') | | T3 | | COMMIT | | T4 | SELECT name FROM user WHERE id < 5 | |Copy the code

Assume the current isolation level is REPEATABLE_READ. First, in transaction A, the set of user names with ids less than 5 is read. Then insert a row with ID 4 and user name golang into transaction B and commit successfully; Then in transaction A, the user name set with ID less than 5 is read again, but this time there is an extra row compared to the user name set read last time. Is this illusion? Phantom reads are similar to unrepeatable reads, but as a result, phantom reads tend to be multi-line recordsets; In terms of data manipulation statements (DML), unrepeatable reads are more associated with UPDATE/DELETE statements, while phantom reads are more associated with INSERT/DELETE statements.

MySQL provides a variety of storage engines, of which only InnoDB storage engine supports transactions and fully follows the ACID principle. After version 5.5 InnoDB replaced MyISAM as the default storage engine. You can use SHOW ENGINES to view these storage ENGINES and their transaction support, as shown below.

| Engine | Support | Transactions | XA | Savepoints | |---------|---------|--------------|-----|------------| | InnoDB |  DEFAULT | YES | YES | YES | | MyISAM | YES | NO | NO | NO | | MEMORY | YES | NO | NO | NO | | CSV | YES | NO | NO | NO | | ARCHIVE | YES | NO | NO | NO |Copy the code

InnoDB supports the four isolation levels defined in the SQL-92 standard without reservation. Where REPEATABLE_READ is its default isolation level. InnoDB typically provides isolation levels at three scopes: Global, Session, and Next Transaction Only. The following describes how each scope is set. global

[mysqld]
transaction-isolation = REPEATABLE-READ
transaction-read-only = OFF
Copy the code

session

| Statements | Legal Transaction Isolations | |---------------------------------------------------------|------------------------------------------------------------- -| | SET @@SESSION.transaction_isolation = 'REPEATABLE-READ' | READ-UNCOMMITTED\READ-COMMITTED\REPEATABLE-READ\SERIALIZABLE | | SET SESSION transaction_isolation = 'REPEATABLE-READ' |  READ UNCOMMITTED\READ COMMITTED\REPEATABLE READ\SERIALIZABLE | | SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ | READ UNCOMMITTED\READ COMMITTED\REPEATABLE READ\SERIALIZABLE |Copy the code

next transaction only

| Statements | Legal Transaction Isolations | |-------------------------------------------------|--------------------------------------------------------------| | SET  @@transaction_isolation = 'REPEATABLE-READ' | READ-UNCOMMITTED\READ-COMMITTED\REPEATABLE-READ\SERIALIZABLE | | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ | READ UNCOMMITTED\READ COMMITTED\REPEATABLE READ\SERIALIZABLE |Copy the code

It is not enough to know how to set the isolation level. You also need to know how to view the isolation level for each scope:

select @@GLOBAL.transaction_isolation;
select @@SESSION.transaction_isolation;
select @@transaction_isolation;
Copy the code

Note: The isolation level in the global scope can also be SET by the SET statement, but forget it.

Next, I will briefly introduce the concepts of commit and rollback. Commit means that changes made to the data by the current transaction are kept permanently and are visible to other transactions; A rollback, in contrast, cancels changes made to the data by the current transaction. In addition, if InnoDB locks are set in the current transaction, commit and rollback will release these locks without hesitation.

For tables in InnoDB, we often use INSERT, UPDATE, and DELETE statements in the DataGrip or Navicat console to manipulate data. We often do not explicitly place these data manipulation statements between START TRANSACTION and COMMIT statements. Why is it still possible to commit automatically? MySQL by default enables autocommit mode for each new connection. In this mode, all SQL statements sent to MySQL are run separately in their own transactions; In other words, each SQL statement is surrounded by a START TRANSACTION statement and a COMMIT statement. Although autoCOMMIT mode is enabled for MySQL connections by default, you can explicitly implement multi-statement transactions using START TRANSACTION and COMMIT statements. You do not need to explicitly disable autoCOMMIT mode by using SET AutoCOMMIT = 0 statements. Once autoCOMMIT mode is off, you must use COMMIT or ROLLBACK statements to close the transaction, otherwise the transaction will remain open. In addition, when autocommit mode is switched from off to on, If autocommit is 0 and you change it to 1, MySQL everyone agrees to perform an automatic COMMIT of any open transaction.

In general, there are two modes for multi-statement transactions in MYSQL, and Spring Transaction is based on MODE I, as shown below:

+--------------------+  +--------------------+
|       MODE I       |  |      MODE II       |
+--------------------+  +--------------------+
+--------------------+  +--------------------+
| SET autocommit = 0 |  | START TRANSACTION  |
+--------------------+  +--------------------+
+--------------------+  +--------------------+
|    BUSINESS SQL    |  |    BUSINESS SQL    |
+--------------------+  +--------------------+
+--------------------+  +--------------------+
|  COMMIT/ROLLBACK   |  |  COMMIT/ROLLBACK   |
+--------------------+  +--------------------+
Copy the code

1 Enable the transaction management mechanism

At work, I often found on the Spring Boot project startup class @ EnableTransactionManagement annotations. In fact, there is no need to explicitly turn on Spring transaction management through this annotation at all, thanks to Spring Boot’s auto-configuration feature. Spring affairs core configuration class is TransactionAutoConfiguration automatically, it is located in the Spring – the boot – autoconfigure module under the transaction package, main logic is as follows:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({DataSourceTransactionManagerAutoConfiguration.class})
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnSingleCandidate(PlatformTransactionManager.class)
    public static class TransactionTemplateConfiguration {
        @Bean
        @ConditionalOnMissingBean(TransactionOperations.class)
        public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
            return newTransactionTemplate(transactionManager); }}@Configuration(proxyBeanMethods = false)
    @ConditionalOnBean(TransactionManager.class)
    @ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
    public static class EnableTransactionManagementConfiguration {
        @Configuration(proxyBeanMethods = false)
        @EnableTransactionManagement(proxyTargetClass = false)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
        public static class JdkDynamicAutoProxyConfiguration {}@Configuration(proxyBeanMethods = false)
        @EnableTransactionManagement(proxyTargetClass = true)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
        public static class CglibAutoProxyConfiguration {}}}Copy the code

The following conclusions can be drawn from the above automatic configuration logic:

  1. TransactionAutoConfiguration by ConditionalOnClass (PlatformTransactionManager. Class), show that the automatic configuration class effective premise: Under current classpath must have PlatformTransactionManager. The figure of a class; In other words, the current classpath must have a spring-TX dependency, which is often easy to satisfy because spring-boot-starter-JDBC depends on spring-JDBC, which in turn depends on Spring-TX.

  2. TransactionTemplateConfiguration is TransactionAutoConfiguration, a static inner class can declare a default type TransactionTemplate Bean; Its by ConditionalOnSingleCandidate (PlatformTransactionManager. Class), shows that the static configuration class effective premise: There is a type of PlatformTransactionManager Bean in the BeanFactory; The Bean will be by spring – the boot – autoconfigure module DataSourceTransactionManagerAutoConfiguration JDBC package under the automatic configuration class declaration.

  3. Same TransactionAutoConfiguration EnableTransactionManagementConfiguration a static inner class, in the static inner class defines two static inner class again: JdkDynamicAutoProxyConfiguration and CglibAutoProxyConfiguration, due to the spring. Aop. Proxy – target – class attribute value defaults to true, So CglibAutoProxyConfiguration will eventually come into effect; Note: CglibAutoProxyConfiguration by @ EnableTransactionManagement (proxyTargetClass = true) annotations, This is “the Spring is no longer needed in the Boot project manually by @ EnableTransactionManagement annotation open transaction management system”, it automatically help us implicit opens the transaction management mechanism.

2 understand @ EnableTransactionManagement annotation

@ EnableTransactionManagement annotation interface content is as follows:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
    boolean proxyTargetClass(a) default false;
    AdviceMode mode(a) default AdviceMode.PROXY;
}
Copy the code
  1. modeProperty is used to specify the weaving mode of aspect logic. There are only two options:PROXYandASPECTJ;PROXY, first create a proxy object for the target object, then apply the section logic to the proxy;ASPECTJ, is a non-runtime weaving mode that directly weaves aspect logic into the target object.
  2. Only if the weave mode isPROXYWhen,proxyTargetClassMake sense; If proxyTargetClass is true, useCGLIBThe proxy generates the proxy class at run time, otherwiseJDKDynamic proxies generate proxy classes at run time.

A more important point: @ EnableTransactionManagement annotations by Import annotations introduced a ImportSelector implementation class, namely TransactionManagementConfigurationSelector. As is known to all, regardless of the Import annotations are introduced by the general Configuration of the @ Configuration modification, or ImportSelector implementation class, or ImportBeanDefinitionRegistrar implementation class, Ultimately, the associated beans are registered with the BeanFactory.

public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {
    @Override
    protected String[] selectImports(AdviceMode adviceMode) {
        switch (adviceMode) {
            case PROXY:
                return new String[] {AutoProxyRegistrar.class.getName(),
                        ProxyTransactionManagementConfiguration.class.getName()};
            case ASPECTJ:
                return new String[] {determineTransactionAspectClass()};
            default:
                return null; }}}Copy the code

From the above, since the weaving mode of section logic is PROXY by default, Therefore selectImports () method returns a directly by AutoProxyRegistrar and ProxyTransactionManagementConfiguration class name of an array of strings. Let’s analyze one by one!

AutoProxyRegistrar AutoProxyRegistrar implements ImportBeanDefinitionRegistrar interface, as shown below:

public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
    private final Log logger = LogFactory.getLog(getClass());
    
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        boolean candidateFound = false;
        Set<String> annTypes = importingClassMetadata.getAnnotationTypes();
        for (String annType : annTypes) {
            AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);
            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) {
                    AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                    if ((Boolean) proxyTargetClass) {
                        AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
                        return;
                    }
                }
            }
        }
    }
}
Copy the code

Seeing the parameters in the registerBeanDefinitions() method above, it should be easy to think, The core logic of the registerBeanDefinitions() method must be to registerBeanDefinition instances with BeanDefinitionRegistry based on what is in AnnotationMetadata. To figure out the details of a registered BeanDefinition instance, you have to start with the first method parameter; So what is AnnotationMetadata? It encapsulates all Spring annotation on a Class of metadata, can generally through AnnotationMetadata. Introspect (Class
type) all the Spring on the way to obtain the target Class metadata annotations. With all this knowledge laid before us, we should realize that: RegisterBeanDefinitions () method of the first parameter is CglibAutoProxyConfiguration encapsulated by the internal static configuration class head all the Spring annotations of metadata, with annotations metadata, Then you can get a @ EnableTransactionManagement annotations in the mode and proxyTargetClass both attribute values, due to the default values for the PROXY mode, Finally with the help of AopConfigUtils BeanDefinitionRegistry to register a beanClass attribute values for InfrastructureAdvisorAutoProxyCreator. BeanDefinition instances of the class, When the BeanDefinition instance is later instantiated, populated, and initialized, The IoC container has a name for the org. Springframework. Aop. Config. InternalAutoProxyCreator, types of InfrastructureAdvisorAutoProxyCreator Bean. You can see the final type of the Bean in your Spring Boot project with the following code:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext configurableApplicationContext = SpringApplication.run(DemoApplication.class, args);
        String AUTO_PROXY_CREATOR_BEAN_NAME = 
                "org.springframework.aop.config.internalAutoProxyCreator"; Object internalAutoProxyCreator = configurableApplicationContext.getBean(AUTO_PROXY_CREATOR_BEAN_NAME); System.out.println(internalAutoProxyCreator.getClass()); }}Copy the code

The print result is as follows:

class org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator
Copy the code

Yi? In the IoC container. There is a name for the org springframework. Aop. Config. InternalAutoProxyCreator Bean instance, But not just said InfrastructureAdvisorAutoProxyCreator type, this is what happened? In fact, there are three important subclasses of Spring AOP abstractA To ProxyCreator, in descending order of priority: abstracta toProxyCreator AnnotationAwareAspectJAutoProxyCreator, AspectJAwareAdvisorAutoProxyCreator and InfrastructureAdvisorAutoProxyCreator; If all three AbstracdefinitionCreators are registered with the BeanDefinitionRegistry, The final will be only AnnotationAwareAspectJAutoProxyCreator resides in BeanDefinitionRegistry, The interested reader can read the registerOrEscalateApcAsRequired AopConfigUtils () method, which involves a priority promotion operations. You can verify the priority of the three by:

public class AutoProxyCreatorPriorityApplication {
    public static final String AUTO_PROXY_CREATOR_BEAN_NAME =
            "org.springframework.aop.config.internalAutoProxyCreator";
    public static void main(String[] args) {
        // STEP 1: Construct BeanDefinitionRegistry
        / / registered in STEP 2, 3, 4, in turn to BeanDefinitionRegistry three AbstractAdvisorAutoProxyCreator BeanDefinition instance,
        // But the name is exactly the same, namely AUTO_PROXY_CREATOR_BEAN_NAME
        BeanDefinitionRegistry beanDefinitionRegistry = new SimpleBeanDefinitionRegistry();

        / / STEP 2: register InfrastructureAdvisorAutoProxyCreator BeanDefinition
        AopConfigUtils.registerAutoProxyCreatorIfNecessary(beanDefinitionRegistry);
        BeanDefinition infrastructureAdvisorCreatorBeanDefinition = beanDefinitionRegistry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
        System.out.println(infrastructureAdvisorCreatorBeanDefinition.getBeanClassName());

        / / STEP 3: register AspectJAwareAdvisorAutoProxyCreator BeanDefinition
        AopConfigUtils.registerAspectJAutoProxyCreatorIfNecessary(beanDefinitionRegistry);
        BeanDefinition aspectJAwareAdvisorCreatorBeanDefinition = beanDefinitionRegistry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
        System.out.println(aspectJAwareAdvisorCreatorBeanDefinition.getBeanClassName());

        / / STEP 4: register AnnotationAwareAspectJAutoProxyCreator BeanDefinitionAopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(beanDefinitionRegistry); BeanDefinition annotationAwareAspectJCreatorBeanDefinition = beanDefinitionRegistry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); System.out.println(annotationAwareAspectJCreatorBeanDefinition.getBeanClassName()); }}Copy the code

Where can AnnotationAwareAspectJAutoProxyCreator is registered into BeanDefinitionRegistry? There is an automatic configuration class named AopAutoConfiguration in the AOP package under the spring-boot-Autoconfigure module, and its main contents are as follows:

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(Advice.class)
    static class AspectJAutoProxyingConfiguration {
        @Configuration(proxyBeanMethods = false)
        @EnableAspectJAutoProxy(proxyTargetClass = false)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
        static class JdkDynamicAutoProxyConfiguration {}@Configuration(proxyBeanMethods = false)
        @EnableAspectJAutoProxy(proxyTargetClass = true)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
        static class CglibAutoProxyConfiguration {}}}Copy the code

Next, enter the EnableAspectJAutoProxy annotation to find out:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
    boolean proxyTargetClass(a) default false;
    boolean exposeProxy(a) default false;
}
Copy the code

To continue, enter AspectJAutoProxyRegistrar find out:

public class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
        AnnotationAttributes enableAspectJAutoProxy =
                AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if(enableAspectJAutoProxy ! =null) {
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) { AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); }}}}Copy the code

Here’s what lushan looks like:

public abstract class AopConfigUtils {
    public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
        return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, null);
    }

    public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) {
        returnregisterOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source); }}Copy the code

ProxyTransactionManagementConfiguration ProxyTransactionManagementConfiguration is a @ Configuration annotation of common Configuration class, Main declares a BeanFactoryTransactionAttributeSourceAdvisor types of beans. As follows:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {
    @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) {
        BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
        advisor.setTransactionAttributeSource(transactionAttributeSource);
        advisor.setAdvice(transactionInterceptor);
        return advisor;
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public TransactionAttributeSource transactionAttributeSource(a) {
        return new AnnotationTransactionAttributeSource();
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) {
        TransactionInterceptor interceptor = new TransactionInterceptor();
        interceptor.setTransactionAttributeSource(transactionAttributeSource);
        if (this.txManager ! =null) {
            interceptor.setTransactionManager(this.txManager);
        }
        returninterceptor; }}Copy the code

There are two branches of Advisor in Spring AOP, PointcutAdvisor and IntroductionAdvisor. PointcutAdvisor holds a Advice and a Pointcut, Spring AOP will Advice modeling for org) aopalliance. Intercept. MethodInterctptor interceptors, Pointcut is used to declare which joinpoints should apply aspect logic. Joinpoints in SpringAOP are specific to method execution, so Advice in Pointcut advisor is a method-level interceptor. The IntroductionAdvisor holds only one Advice and a ClassFilter. Obviously, the Advice in the IntroductionAdvisor is a class-level interceptor. Obviously, the PointcutAdvisor is more commonly used because the Pointcut in the PointcutAdvisor contains not only classFilters, but also MethodMatcher, You can use the Matches () method in MethodMatcher to exactly intercept a target method that holds the @Transactional annotation.

From the point of BeanFactoryTransactionAttributeSourceAdvisor inheritance diagram, it belongs to PointcutAdvisor branch. From the above ProxyTransactionManagementConfiguration transactionAdvisor () method of the content, Create BeanFactoryTransactionAttributeSourceAdvisor instance, followed by TransactionInterceptor TransactionAttributeSource placement in this instance; Among them, the TransactionInterceptor will not take the Advice of PointcutAdvisor role, and TransactionAttributeSource certainly related to the Pointcut. BeanFactoryTransactionAttributeSourceAdvisor source must can verify this claim:

public class BeanFactoryTransactionAttributeSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {

    private TransactionAttributeSource transactionAttributeSource;

    private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() {
        @Override
        protected TransactionAttributeSource getTransactionAttributeSource(a) {
            returntransactionAttributeSource; }};public void setTransactionAttributeSource(TransactionAttributeSource transactionAttributeSource) {
        this.transactionAttributeSource = transactionAttributeSource; }}Copy the code

Is the source that TransactionAttributeSource Pointcut a big arms, also revealed Pointcut looks like: TransactionAttributeSourcePointcut, its main content is as follows:

abstract class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointcut {
    protected TransactionAttributeSourcePointcut(a) {
        setClassFilter(new TransactionAttributeSourceClassFilter());
    }
    
    @Override
    public boolean matches(Method method, Class
        targetClass) {
        TransactionAttributeSource tas = getTransactionAttributeSource();
        return (tas == null|| tas.getTransactionAttribute(method, targetClass) ! =null);
    }

    protected abstract TransactionAttributeSource getTransactionAttributeSource(a);

    private class TransactionAttributeSourceClassFilter implements ClassFilter {
        @Override
        public boolean matches(Class
        clazz) {
            if (TransactionalProxy.class.isAssignableFrom(clazz) ||
                    TransactionManager.class.isAssignableFrom(clazz) ||
                    PersistenceExceptionTranslator.class.isAssignableFrom(clazz)) {
                return false;
            }
            TransactionAttributeSource tas = getTransactionAttributeSource();
            return (tas == null|| tas.isCandidateClass(clazz)); }}}Copy the code

TransactionAttributeSourcePointcut not only inherited the StaticMethodMatcherPointcut, inside another ClassFilter type of inner class: Collected TransactionAttributeSourceClassFilter, so this Pointcut ClassFilter and MethodMatcher. MethodMatcher matches(Method Method, Class
targetClass) method and matches(Class
clazz) methods are dependent on getTransactionAttributeSource () method returns the TransactionAttributeSource instance, This instance is AnnotationTransactionAttributeSource, its main content is:

public class AnnotationTransactionAttributeSource extends AbstractFallbackTransactionAttributeSource {
    private static final boolean jta12Present;

    private static final boolean ejb3Present;

    static {
        ClassLoader classLoader = AnnotationTransactionAttributeSource.class.getClassLoader();
        jta12Present = ClassUtils.isPresent("javax.transaction.Transactional", classLoader);
        ejb3Present = ClassUtils.isPresent("javax.ejb.TransactionAttribute", classLoader);
    }

    private final boolean publicMethodsOnly;

    private final Set<TransactionAnnotationParser> annotationParsers;

    public AnnotationTransactionAttributeSource(a) {
        this(true);
    }

    public AnnotationTransactionAttributeSource(boolean publicMethodsOnly) {
        this.publicMethodsOnly = publicMethodsOnly;
        if (jta12Present || ejb3Present) {
            this.annotationParsers = new LinkedHashSet<>(4);
            this.annotationParsers.add(new SpringTransactionAnnotationParser());
            if (jta12Present) {
                this.annotationParsers.add(new JtaTransactionAnnotationParser());
            }
            if (ejb3Present) {
                this.annotationParsers.add(newEjb3TransactionAnnotationParser()); }}else {
            this.annotationParsers = Collections.singleton(newSpringTransactionAnnotationParser()); }}@Override
    public boolean isCandidateClass(Class
        targetClass) {
        for (TransactionAnnotationParser parser : this.annotationParsers) {
            if (parser.isCandidateClass(targetClass)) {
                return true; }}return false;
    }

    @Override
    protected TransactionAttribute findTransactionAttribute(Class
        clazz) {
        return determineTransactionAttribute(clazz);
    }

    @Override
    @Nullable
    protected TransactionAttribute findTransactionAttribute(Method method) {
        return determineTransactionAttribute(method);
    }

    protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element) {
        for (TransactionAnnotationParser parser : this.annotationParsers) {
            TransactionAttribute attr = parser.parseTransactionAnnotation(element);
            if(attr ! =null) {
                returnattr; }}return null;
    }

    /** * By default, only public methods can be made transactional. */
    @Override
    protected boolean allowPublicMethodsOnly(a) {
        return this.publicMethodsOnly; }}Copy the code

According to the above AnnotationTransactionAttributeSource source content, can be obtained:

  1. In constructing TransactionAttributeSource instance, will fill itSet<TransactionAnnotationParser>Type member variable annotationParsers, generallySpringTransactionAnnotationParserandJtaTransactionAnnotationParserThese two parsers.
  2. isCandidateClass()The methods andfindTransactionAttribute()Methods delegateTransactionAnnotationParserDevelop specific logic.
  3. publicMethodsOnlyThe default value is true@TransactionalAnnotations are only annotated in the access control modifier forpublicIs effective.

JtaTransactionAnnotationParser is responsible for parsing javax.mail. Transaction. Transactional annotation, While SpringTransactionAnnotationParser is responsible for parsing org. Springframework. Transaction. The annotation. Transactional annotation, so the latter is our focus. SpringTransactionAnnotationParser source code is as follows:

public class SpringTransactionAnnotationParser implements TransactionAnnotationParser {
    @Override
    public boolean isCandidateClass(Class
        targetClass) {
        return AnnotationUtils.isCandidateClass(targetClass, Transactional.class);
    }

    @Override
    public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {
        AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(
                element, Transactional.class, false.false);
        if(attributes ! =null) {
            return parseTransactionAnnotation(attributes);
        } else {
            return null; }}public TransactionAttribute parseTransactionAnnotation(Transactional ann) {
        return parseTransactionAnnotation(AnnotationUtils.getAnnotationAttributes(ann, false.false));
    }

    protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) {
        RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute();

        Propagation propagation = attributes.getEnum("propagation");
        rbta.setPropagationBehavior(propagation.value());
        Isolation isolation = attributes.getEnum("isolation");
        rbta.setIsolationLevel(isolation.value());

        rbta.setTimeout(attributes.getNumber("timeout").intValue());
        String timeoutString = attributes.getString("timeoutString");
        rbta.setTimeoutString(timeoutString);

        rbta.setReadOnly(attributes.getBoolean("readOnly"));
        rbta.setQualifier(attributes.getString("value"));
        rbta.setLabels(Arrays.asList(attributes.getStringArray("label")));

        List<RollbackRuleAttribute> rollbackRules = new ArrayList<>();
        for(Class<? > rbRule : attributes.getClassArray("rollbackFor")) {
            rollbackRules.add(new RollbackRuleAttribute(rbRule));
        }
        for (String rbRule : attributes.getStringArray("rollbackForClassName")) {
            rollbackRules.add(new RollbackRuleAttribute(rbRule));
        }
        for(Class<? > rbRule : attributes.getClassArray("noRollbackFor")) {
            rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
        }
        for (String rbRule : attributes.getStringArray("noRollbackForClassName")) {
            rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
        }
        rbta.setRollbackRules(rollbackRules);

        returnrbta; }}Copy the code

The logic is clear, the main is to @ all Transactional annotation attribute values in Spring parsing out, then encapsulated in a RuleBasedTransactionAttribute instance.

3 Transaction Attributes

In the Spring in the Transaction, TransactionDefinition interface is one of the top abstraction of Transaction attribute, RuleBasedTransactionAttribute is its most common implementation class. In addition to isolation levels, Transaction timeouts, and whether or not a read-only Transaction, Spring Transaction extends Transaction attributes to propagate behavior and rollback rules. In a declarative transaction scenario, the values of these five Transactional attributes come from the @Transactional annotation.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    Propagation propagation(a) default Propagation.REQUIRED;
    Isolation isolation(a) default Isolation.DEFAULT;
    int timeout(a) default TransactionDefinition.TIMEOUT_DEFAULT;
    boolean readOnly(a) default false;
    Class<? extends Throwable>[] rollbackFor() default {};
}
Copy the code

3.1 Isolation Level

The DEFAULT Isolation level is isolation. DEFAULT, that is, the current Isolation level of the database is used by DEFAULT. If the database is configured with read-COMMITTED isolation level and the ISOLATION property specifies REPEATABLE-READ, the transaction’s isolation level is ultimately REPEATABLE-READ. The isolation level is entirely dependent on the implementation of the database itself; For MySQL, Spring sets the isolation level for the current transaction with the following statement.

SET SESSION TRANSACTION ISOLATION LEVEL ISOLATION_LEVEL
Copy the code

3.2 Whether a read-only transaction is required

Whether a read-only transaction defaults to false; INSERT, UPDATE, and DELETE statements are not allowed in read-only transactions. Otherwise, an error is reported: Cannot execute Statement in a READ ONLY transaction. Similarly, read-only transactions depend entirely on the implementation of the database itself, and Spring has no logic for that; In MySQL, read-only transactions avoid the overhead of generating transaction ids; For MySQL, Spring sets the current transaction to read-only mode by using the following statement.

SET SESSION TRANSACTION READ ONLY
Copy the code

3.3 Communication Behavior

In fact, propagation behavior stays at the Spring level, and there is no concept in the database system.

PROPAGATION_REQUIRED If an outer transaction exists, join it. Otherwise, a transaction is created. PROPAGATION_SUPPORTS Adds an outer transaction if it exists. Otherwise, run nontransactionally. PROPAGATION_MANDATORY If an outer transaction exists, it is added. Otherwise, an exception is thrown. PROPAGATION_REQUIRES_NEW Suspends an outer transaction if one exists. Otherwise, a transaction is created. PROPAGATION_NOT_SUPPORTED Always runs non-transactionally, regardless of whether an outer transaction exists. Throw an exception if an outer transaction exists, PROPAGATION_NEVER Otherwise, run nontransactionally. PROPAGATION_NESTED Runs as a nested transaction if an outer transaction exists. Otherwise, a transaction is created.Copy the code

Of the seven propagation behaviors, PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, and PROPAGATION_NESTED are the most commonly used. For PROPAGATION_REQUIRED and PROPAGATION_NESTED, the inner and outer layer transaction methods share a java.sql.Connection instance object. For PROPAGATION_REQUIRES_NEW, the inner and outer layer transaction methods each hold a java.sql.Connection instance object. In addition, with the help of PROPAGATION_NESTED, outer transactions can still commit normally even if the inner nested transaction is rolled back, which can be simulated by the following SQL statement (the ROLLBACK TO SAVEPOINT statement can be omitted if the inner nested transaction is committed normally).

SET autocommit = 0;
INSERT INTO tbl_a(id, name) VALUE ('1'.'java');
SAVEPOINT `SAVEPOINT_1`;
INSERT INTO tbl_b(id, name) VALUE ('1'.'golang');
ROLLBACK TO SAVEPOINT `SAVEPOINT_1`;
COMMIT;
Copy the code

All savepoints of the current transaction are deleted if you execute a COMMIT, or a ROLLBACK that does not name a savepoint.

3.4 Rollback Rules

Spring Transaction follows a rollback rule based on exceptions, so which exceptions? As follows:

public class DefaultTransactionAttribute extends DefaultTransactionDefinition implements TransactionAttribute {
    @Override
    public boolean rollbackOn(Throwable ex) {
        return (ex instanceof RuntimeException || ex instanceofError); }}Copy the code

By default, a transaction rollback is triggered when a RuntimeException or ERROR is thrown within a transaction method.

4 Transaction Synchronization

Before introducing transaction synchronization, let’s look at two pieces of code.

  1. In the scenario of registered users, after persistent users are created, a series of operations, such as granting credits to new users and opening email addresses, need to be notified to consumers through message queues.
@Service
public class DemoService {
    @Transactional
    public void demoMtd(a) {
        userMaper.insert(user);
        rabbitTemplate.convertAndSend("exchange"."routingKey", user); }}Copy the code

Imagine that when the consumer listens to the message, he goes to the user table and queries the user. Can he find the data? Because one transaction cannot read uncommitted data from another transaction; If you read it, I suggest you take the DBA to heaven. But read the business that is less than likely to consume square to spread out to have an effect somewhat, what method can be solved? Removing @Transactional is fine, but it’s not rigorous. Solutions are as follows:

@Service
public class DemoService {
    @Transactional
    public void demoMtd(a) {
        userMaper.insert(user);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit(a) {
                rabbitTemplate.convertAndSend("exchange"."routingKey", user); }}); }}Copy the code
  1. It is common practice to start a separate thread outside the trunk thread to handle tasks loosely coupled to the trunk business logic.
@Service
public class DemoService {
    @Transactional
    public void demoMtd(a) {
        userMaper.insert(user);
        new Thread(new Runnable() {
            @Override
            public void run(a) {
                User _user = userMapper.selectByPrimaryKey("userId");
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch(InterruptedException e) { e.printStackTrace(); }}}Copy the code

In the above code, first insert a record in the main thread, and then try to query the record in another thread. Can this be found? Again, the answer is no. The root cause is that two java.sqL. Connection instances were created in each thread, and the trunk thread and the other path were running in two transactions. Why must two threads each create a java.sql.Connection instance? Remember: JDBC API in Java. SQL. Org. In Connection, Mybatis apache. Ibatis. Session. The SqlSession and Hibernate in the org. Hibernate. The session all thread unsafe class, Two threads must not share the same Connection, SqlSession, or Session instance. The solution is similar to the following:

@Service
public class DemoService {
    @Transactional
    public void demoMtd(a) {
        userMaper.insert(user);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit(a) {
                new Thread(new Runnable() {
                    @Override
                    public void run(a) {
                        User _user = userMapper.selectByPrimaryKey("userId"); } }).start(); }});try {
            TimeUnit.SECONDS.sleep(2);
        } catch(InterruptedException e) { e.printStackTrace(); }}}Copy the code

Obviously, TransactionSynchronization callback interface is a standard. Spring 5.3 of all the methods in the interface for a default keyword and then implement this interface, it is not necessary to implement all the methods, there would be no need for TransactionSynchronizationAdapter nature also the adapter to the ha. The main contents are as follows:

public interface TransactionSynchronization {
    // Suspend this synchronization. Supposed to unbind resources from TransactionSynchronizationManager.
    default void suspend(a) {}
    // Resume this synchronization. Supposed to rebind resources to TransactionSynchronizationManager.
    default void resume(a) {}
    
    default void beforeCommit(boolean readOnly) {}
    default void beforeCompletion(a) {}
    default void afterCommit(a) {}
    default void afterCompletion(int status) {}}Copy the code

TransactionSynchronization, Chinese translation for the transaction. Sync for whom? Transactions, of course. So sync what? One is TransactionSynchronization ontology; Another is the so-called “resource”, resources can be JDBC API in Java, SQL, the Connection, the Mybatis org. Apache. Ibatis. Session. The SqlSession, Org.hibernate. Session in Hibernate. TransactionSynchronization from belongs to a Transaction, only the Spring Transaction will not be a TransactionSynchronization synchronous (binding) to the two transactions; In other words, the callback logic TransactionSynchronization will only run in a transaction.

For developers, the afterCommit() method is the most common. The official recommendation is to use this method to host some type of logic such as send mail, send messages, etc., which is not suitable for database oriented insert and update operations.

After afterCommit() and afterCompletion() are completed, Spring Transaction restarts autoCOMMIT mode for the current connection.

Declarative transaction core source code interpretation

PlatformTransactionManager is one of the top abstract transaction management, it will first according to the propagation behavior of TransactionDefinition TransactionStatus, Then commit or roll back based on what’s in TransactionStatus. The Spring transaction management core class diagram looks like this:

For unified programming model of Transaction management, Spring Transaction will PlatformTransactionManager is designed as a SPI, at the same time in order to reduce the JDBC, Hibernate, JPA and JTA platform workload, facilitate each platform reuse based logic, Spring on the basis of this and provides an abstract class: AbstractPlatformTransactionManager. You may realize the difference between using interfaces for abstraction and using abstract classes for reuse.

In the this article second chapter BeanFactoryTransactionAttributeSourceAdvisor once mentioned, with the PointcutAdvisor can for those who contain @ Transactional annotation of the Transactional beans to create the proxy class, Ultimately, it is the proxy class that resides in the IoC, so that when other beans invoke transactional methods in the transactional Bean, the proxy class intercepts them and executes the enhanced logic in the TransactionInterceptor. For proxy class generation logic, see the following figure:

The TransactionInterceptor implements methodtor. The enhanced logic of the proxy class must fall on the invoke() method. The core content is as follows:

public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable { Class<? > targetClass = (invocation.getThis() ! =null ? AopUtils.getTargetClass(invocation.getThis()) : null);
        return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
            @Override
            public Object proceedWithInvocation(a) throws Throwable {
                return invocation.proceed();
            }
            @Override
            public Object getTarget(a) {
                return invocation.getThis();
            }
            @Override
            public Object[] getArguments() {
                returninvocation.getArguments(); }}); }}Copy the code

Next, go to the invokeWithinTransaction() method in TransactionAspectSupport:

public abstract class TransactionAspectSupport {
    protected Object invokeWithinTransaction(Method method, Class<? > targetClass,final InvocationCallback invocation) throws Throwable {
        // Get the TransactionAttribute parser
        TransactionAttributeSource tas = getTransactionAttributeSource();
        // Get the TransactionAttribute directly from attributeCache, without recalculating
        final TransactionAttribute txAttr = tas.getTransactionAttribute(method, targetClass);
        / / get TransactionManager
        final TransactionManager tm = determineTransactionManager(txAttr);
        // Get the transaction name
        final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
        // If non-transactional method or non-JTA transaction
        if (txAttr == null| |! (tminstanceof CallbackPreferringPlatformTransactionManager)) {
            Create a transaction object based on the transaction propagation behavior in TransactionAttribute
            TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
            // Target method execution result
            Object retVal;
            try {
                // Execute the target method
                retVal = invocation.proceedWithInvocation();
            } catch (Throwable ex) {
                // Rollback the transaction
                completeTransactionAfterThrowing(txInfo, ex);
                // Throw an exception again
                throw ex;
            } finally {
                TransactionAspectSupport has a member variable of type ThreadLocal, transactionInfoHolder
                // oldTransactionInfo holds an oldTransactionInfo member variable
                CleanupTransactionInfo basically updates the Value of the transactionInfoHolder
                cleanupTransactionInfo(txInfo);
            }
            // Commit the transaction
            commitTransactionAfterReturning(txInfo);
            // Returns the result of the target method execution
            returnretVal; }}}Copy the code

The invokeWithinTransaction() method Outlines the overall logic of a declarative transaction, as shown below:

In particular, we need to createTransactionIfNecessary (), commitTransactionAfterReturning () and completeTransactionAfterThrowing () is one by one, analyzes the three methods.

5.1 createTransactionIfNecessary ()

public abstract class TransactionAspectSupport {
    protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, String joinpointIdentification) {
        TransactionStatus status = null;
        if(txAttr ! =null) {
            if(tm ! =null) {
                // Get the TransactionStatus instance from TransactionManagerstatus = tm.getTransaction(txAttr); }}// Encapsulate TransactionStatus and TransactionAttribute as TransactionInfo instances
        // TransactionInfo instances will continue throughout the transaction management lifecycle
        returnprepareTransactionInfo(tm, txAttr, joinpointIdentification, status); }}Copy the code

Enter the getTransaction() method:

public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager {
    @Override
    public final TransactionStatus getTransaction(TransactionDefinition definition) {
        // If definition is null, the default TransactionDefinition instance is usedTransactionDefinition def = (definition ! =null ? definition : TransactionDefinition.withDefaults());
        / / create a transaction object, usually DataSourceTransactionObject
        // Note: Even if the inner and outer layer transactions share a java.sqL. Connection instance, two transaction objects are created
        Object transaction = doGetTransaction();
        // Determine whether a transaction currently exists
        // If definition comes from an outer transaction method, there can be no transaction
        If definition comes from an in-memory transaction method, there may be an outer transaction
        if (isExistingTransaction(transaction)) {
            // Since there are outer transactions, different processing strategies need to be adopted based on the transaction propagation behavior declared by the inner transaction method
            // 1) Throw an exception, PROPAGATION_NEVER
            // 2) Suspend the outer transaction, PROPAGATION_NOT_SUPPORTED
            // 3) For PROPAGATION_REQUIRES_NEW, suspend the outer transaction and start a new one
            // create savePoint for PROPAGATION_NESTED
            // 5) For PROPAGATION_SUPPORTS and PROPAGATION_REQUIRED, add the outer transaction directly
            return handleExistingTransaction(def, transaction, debugEnabled);
        }
        // The definition is derived from the outer transaction method
        // If the transaction propagation behavior declared by the outer transaction method is PROPAGATION_MANDATORY, throw an exception
        if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
            throw new IllegalTransactionStateException("No existing transaction found for transaction marked with propagation 'mandatory'");
        } else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
                def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
                def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
            // The propagation behavior declared by the outer transaction method must be PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, and PROPAGATION_NESTED
            // Start a new transaction
            return startTransaction(def, transaction, debugEnabled, suspendedResources);
        } else {
            // The external transaction method can only declare the following transaction propagation behavior:
            / / 1) PROPAGATION_NEVER
            / / 2) PROPAGATION_NOT_SUPPORTED
            / / 3) PROPAGATION_SUPPORTS
            // The outer transaction method declares that the transaction propagation behavior belongs to one of the above three categories
            // Enable empty transactions
            boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
            return prepareTransactionStatus(def, null.true, newSynchronization, debugEnabled, null); }}}Copy the code

Please read handleExistingTransaction itself () method, the content of here only reading startTransaction () method:

public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager {
    private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
                                               boolean debugEnabled, SuspendedResourcesHolder suspendedResources) {
        booleannewSynchronization = (getTransactionSynchronization() ! = SYNCHRONIZATION_NEVER); DefaultTransactionStatus status = newTransactionStatus( definition, transaction,true, newSynchronization, debugEnabled, suspendedResources);
        doBegin(transaction, definition);
        prepareSynchronization(status, definition);
        returnstatus; }}Copy the code

Enter the doBegin() method:

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager {
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        Connection con = null;
        try {
            if(! txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { Connection newCon = obtainDataSource().getConnection(); txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
            }

            txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
            con = txObject.getConnectionHolder().getConnection();

            Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
            txObject.setPreviousIsolationLevel(previousIsolationLevel);
            txObject.setReadOnly(definition.isReadOnly());
            
            if (con.getAutoCommit()) {
                txObject.setMustRestoreAutoCommit(true);
                con.setAutoCommit(false);
            }

            prepareTransactionalConnection(con, definition);
            txObject.getConnectionHolder().setTransactionActive(true);

            int timeout = determineTimeout(definition);
            if(timeout ! = TransactionDefinition.TIMEOUT_DEFAULT) { txObject.getConnectionHolder().setTimeoutInSeconds(timeout); }// Bind the connection holder to the thread.
            if(txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); }}catch (Throwable ex) {
            if (txObject.isNewConnectionHolder()) {
                DataSourceUtils.releaseConnection(con, obtainDataSource());
                txObject.setConnectionHolder(null.false);
            }
            throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); }}}Copy the code

The important logic in the doBegin() method is:

  1. Establish a connection and associate it with a transaction object;
Connection newCon = obtainDataSource().getConnection();
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
Copy the code
  1. Set transaction isolation level and whether read-only transaction;
public class DataSourceUtils {
    public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) throws SQLException {
        boolean debugEnabled = logger.isDebugEnabled();
        // Set read-only flag.
        if(definition ! =null && definition.isReadOnly()) {
            try {
                if (debugEnabled) {
                    logger.debug("Setting JDBC Connection [" + con + "] read-only");
                }
                con.setReadOnly(true);
            } catch (SQLException | RuntimeException ex) {
                // "read-only not supported" SQLException -> ignore, it's just a hint anyway
                logger.debug("Could not set JDBC Connection read-only", ex); }}// Apply specific isolation level, if any.
        Integer previousIsolationLevel = null;
        if(definition ! =null&& definition.getIsolationLevel() ! = TransactionDefinition.ISOLATION_DEFAULT) {if (debugEnabled) {
                logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " + definition.getIsolationLevel());
            }
            int currentIsolation = con.getTransactionIsolation();
            if (currentIsolation != definition.getIsolationLevel()) {
                previousIsolationLevel = currentIsolation;
                con.setTransactionIsolation(definition.getIsolationLevel());
            }
        }
        returnpreviousIsolationLevel; }}Copy the code
  1. Disable autocommit mode.
con.setAutoCommit(false);
Copy the code
  1. Binds the current connection object to the current thread
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
Copy the code

5.2 commitTransactionAfterReturning ()

public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager {
    private void processCommit(DefaultTransactionStatus status) throws TransactionException {
        try {
            boolean beforeCompletionInvoked = false;
            try {
                boolean unexpectedRollback = false;
                prepareForCommit(status);
                triggerBeforeCommit(status);
                triggerBeforeCompletion(status);
                beforeCompletionInvoked = true;

                if (status.hasSavepoint()) {
                    if (status.isDebug()) {
                        logger.debug("Releasing transaction savepoint");
                    }
                    unexpectedRollback = status.isGlobalRollbackOnly();
                    status.releaseHeldSavepoint();
                } else if (status.isNewTransaction()) {
                    if (status.isDebug()) {
                        logger.debug("Initiating transaction commit");
                    }
                    unexpectedRollback = status.isGlobalRollbackOnly();
                    doCommit(status);
                } else if (isFailEarlyOnGlobalRollbackOnly()) {
                    unexpectedRollback = status.isGlobalRollbackOnly();
                }

                // Throw UnexpectedRollbackException if we have a global rollback-only
                // marker but still didn't get a corresponding exception from commit.
                if (unexpectedRollback) {
                    throw new UnexpectedRollbackException("Transaction silently rolled back because it has been marked as rollback-only"); }}catch (UnexpectedRollbackException ex) {
                // can only be caused by doCommit
                triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
                throw ex;
            } catch (TransactionException ex) {
                // can only be caused by doCommit
                if (isRollbackOnCommitFailure()) {
                    doRollbackOnCommitException(status, ex);
                } else {
                    triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
                }
                throw ex;
            } catch (RuntimeException | Error ex) {
                if(! beforeCompletionInvoked) { triggerBeforeCompletion(status); } doRollbackOnCommitException(status, ex);throw ex;
            }
            // 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); }}}Copy the code

Now look at the doCommit() method:

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager {
    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();
        if (status.isDebug()) {
            logger.debug("Committing JDBC transaction on Connection [" + con + "]");
        }
        try {
            con.commit();
        } catch (SQLException ex) {
            throw translateException("JDBC commit", ex); }}}Copy the code

5.3 completeTransactionAfterThrowing ()

public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager {
    private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
        try {
            boolean unexpectedRollback = unexpected;
            try {
                triggerBeforeCompletion(status);

                if (status.hasSavepoint()) {
                    if (status.isDebug()) {
                        logger.debug("Rolling back transaction to savepoint");
                    }
                    status.rollbackToHeldSavepoint();
                } else if (status.isNewTransaction()) {
                    if (status.isDebug()) {
                        logger.debug("Initiating transaction rollback");
                    }
                    doRollback(status);
                } else {
                    // Participating in larger transaction
                    if (status.hasTransaction()) {
                        if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                            if (status.isDebug()) {
                                logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                            }
                            doSetRollbackOnly(status);
                        } else {
                            if (status.isDebug()) {
                                logger.debug("Participating transaction failed - letting transaction originator decide on rollback"); }}}else {
                        logger.debug("Should roll back transaction but cannot - no transaction available");
                    }
                    // Unexpected rollback only matters here if we're asked to fail early
                    if(! isFailEarlyOnGlobalRollbackOnly()) { unexpectedRollback =false; }}}catch (RuntimeException | Error ex) {
                triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
                throw ex;
            }
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
            // Raise UnexpectedRollbackException if we had a global rollback-only marker
            if (unexpectedRollback) {
                throw new UnexpectedRollbackException("Transaction rolled back because it has been marked as rollback-only"); }}finally{ cleanupAfterCompletion(status); }}}Copy the code

Now look at the doRollback() method:

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager {
    @Override
    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 translateException("JDBC rollback", ex); }}}Copy the code

6. Summary

I believe that after reading this article, you will have a deeper understanding of Spring declarative transaction management, but there will certainly be many questions, which requires you to read the source code. Finally, two points are emphasized: one is to correctly understand the propagation behavior of transactions; Do not use remote calls in the @Transactional method. This can lead to large transactions, which can lead to long rollback times and database connection pool depletion. Therefore, it is recommended to use programmatic transaction management.

Reference documentation

  1. Dev.mysql.com/doc/refman/…
  2. Dev.mysql.com/doc/refman/…
  3. Dev.mysql.com/doc/refman/…
  4. Docs. Spring. IO/spring – fram…