Deep understanding of Spring declarative transactions

Problem introduction

  1. What are the types of transaction propagation in Spring?
  2. Understand the automatic configuration of annotation transactions?
  3. Why doesn't the SpringBoot startup class need the @ EnableTransactionManagement annotation?
  4. How does declarative transaction work? (to be supplemented)

1 declarative transaction

It is inevitable to deal with data in system development, and transaction management is essential. Spring supports declarative transactions, and controls whether the method supports transactions through @ Transactional annotations. Declarative transaction, based on AOP implementation, decouples specific business and business logic.

Spring provides @ EnableTransactionManagement annotation to enable support transactions on the configuration class (startup class). At this time, spring will automatically scan the classes and methods with @ Transactional annotation. This annotation is equivalent to < TX: annotation driven / > of xml configuration mode. By setting the mode attribute, you can decide whether to use spring proxy or ASPECTJ extension.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
   boolean proxyTargetClass() default false;
   AdviceMode mode() default AdviceMode.PROXY; // proxy pattern
   int order() default Ordered.LOWEST_PRECEDENCE; // LOWEST_ Precision has the lowest priority, so it is at the outermost part of the execution chain, and the priority of the AOP interceptors implemented by ourselves is higher than that of transactions, so they are nested inside and closer to the business code.
}

2 use of @ transactional annotation

2.1 @Transactional annotation attribute

@Transactional annotations can be applied to classes and methods. When declaring a class, the annotation is applied to all methods of the class and subclasses by default, and it is only effective when applied to public methods; The parent class method needs to be declared separately if it needs to be annotated at the same level.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

	@AliasFor("transactionManager")
	String value() default "";
    
    // Used to determine the target transaction manager
	@AliasFor("value")
	String transactionManager() default "";

    // Propagation of transactions, default Propagation.REQUIRED
	Propagation propagation() default Propagation.REQUIRED;

    // Transaction isolation level. The default is Isolation.DEFAULT
	Isolation isolation() default Isolation.DEFAULT;

    // Transaction timeout. The default is - 1
	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    // Specifies whether the transaction is read-only. The default value is false. It is just a prompt
	boolean readOnly() default false;

    // Identify the exception type that can trigger transaction rollback. The default is RuntimeException and Error, excluding check exceptions.
	Class<? extends Throwable>[] rollbackFor() default {};

	// Identify which exceptions do not require a rollback transaction
	Class<? extends Throwable>[] noRollbackFor() default {};

	String[] noRollbackForClassName() default {};
	String[] rollbackForClassName() default {};
}

Among them, the isolation and timeout attributes are only valid for newly started transactions, specifically Propagation.REQUIRED and propagation.requirements_ Designed for new use.

2.2 Propagation of transactions

Propagation defines the propagation of transactions at seven levels.

public enum Propagation {
	REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
	SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
	MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
	NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
	NEVER(TransactionDefinition.PROPAGATION_NEVER),
	NESTED(TransactionDefinition.PROPAGATION_NESTED);

	private final int value;

	Propagation(int value) {
		this.value = value;
	}

	public int value() {
		return this.value;
	}
}
  • REQUIRED: use the current transaction. If there is no transaction, create a new transaction. The sub method must run in a transaction; If there is a transaction, join the transaction as a whole.
    For example: if the leader has no food and I have money, I will buy it myself; If some leaders eat, they will share it with you.
  • SUPPORTS: if there is a transaction currently, the transaction is used; If there are currently no transactions, no transactions are used. Mostly used for query.
    For example: the leader has no food, and I have no food; Leaders have food, so do I.
  • MANDATORY: the propagation attribute enforces the existence of a transaction. If it does not exist, an exception will be thrown.
    For example: the leader must take care of the food. No matter what the food is, there is no food to eat, so I quit (an exception will be thrown).
  • REQUIRES_NEW: if there is a current transaction, suspend the transaction and create a new transaction for your own use; If there is no current transaction, the same as REQUIRED
    For example: leaders have food, but I don't want it. I buy and eat by myself
    • 1. Sign requirements_ New will start a new transaction, and the outer transaction will not affect the commit / rollback of the internal transaction. Submitting modifications internally will lead to dirty reading of A.
    • 2. Sign requirements_ The internal transaction of new is abnormal, which will affect the rollback of external transactions
  • NOT_SUPPORTED: if there is a transaction, suspend the transaction and run the database operation without using the transaction
    For example: leaders have food to eat. I'll give you some. I'm too busy. Put it aside and I won't eat
  • NEVER: throw an exception if a transaction currently exists
    For example: the leader has food for you. I don't want to eat. I throw an exception decisively
  • NESTED: if there is a transaction currently, open the sub transaction (NESTED transaction); If there is no current transaction, the same as REQUIRED. However, if the primary transaction is committed, it will be committed with the secondary transaction. If the primary transaction is rolled back, the child transactions are rolled back together. Conversely, if the child transaction is abnormal, the parent transaction can be rolled back or not rolled back (try catch).
    For example: the leader makes a wrong decision, the boss blames, and the leader suffers with his little brother; If the younger brother makes a mistake, the leader can shirk the responsibility.

Distinguish between NESTED and requirements_ NEW

The most fundamental difference is that NESTED is still in a transaction, but submitted together with the main task.

// TransactionalServiceImpl
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void testPropagation() {
    stuService.saveParent();
    stuService.saveChildren();
    int a = 1 / 0;
}

// StuServiceImpl
/* Test transaction propagation */
@Transactional(propagation = Propagation.NESTED) // Toggle nested / requirements_ NEW
@Override
public void saveParent() {
	Stu stu = new Stu();
	stu.setName("parent");
	stu.setScore(100);
	stuMapper.insert(stu);
}

@Transactional(propagation = Propagation.NESTED)
@Override
public void saveChildren() {
	saveChild1();
	int a = 1 / 0;
	saveChild2();
}

An easily overlooked point

In proxy mode (default), only incoming external method calls through the proxy are intercepted. This means that method calls within the same target object, even if the called method is marked with @ Transactional, will not cause transaction interception at run time.

// Same class
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void saveChildren() {
    saveChild1();
    int a = 1 / 0;
    saveChild2();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveChild1() {
    Stu stu = new Stu();
    stu.setName("child-1");
    stu.setScore(60);
    stuMapper.insert(stu);
}

Refer to official documents

In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation, in effect, a method within the target object calling another method of the target object, will not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional. Also, the proxy must be fully initialized to provide the expected behaviour so you should not rely on this feature in your initialization code, i.e. @PostConstruct.
In proxy mode (default), only external method calls that enter through the proxy are intercepted. This means that a self call is actually a method in the target object that calls another method of the target object, even if the called method is used@Transactional Tags also do not cause actual transactions at run time. In addition, the agent must be fully initialized to provide the expected behavior, so you should not rely on this feature in the initialization code, that is, the@PostConstruct. 

2.3 Isolation level of transaction - Isolation

public enum Isolation {
	DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
	READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED), 
	READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED), 
	REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ), 
	SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);

	private final int value;

	Isolation(int value) {
		this.value = value;
	}

	public int value() {
		return this.value;
	}
}

There are 5 transaction isolation levels in Spring. The setting of isolation level depends on whether the current database supports it.

  • DEFAULT: use the DEFAULT isolation level of the current database. For example, Oracle is read_ Committed, MySQL is READ_REPEATED.
  • READ_ Uncommitted: it can cause dirty reading, non repeatable reading and phantom reading.
  • READ_COMMITTED: prevent dirty reading, which can lead to non repeatable reading and unreal reading.
  • REPEATABLE_READ: prevent dirty reading and non repeatable reading, which can lead to unreal reading.
  • SERIALIZABLE: transactions at this level are executed sequentially to prevent the above defects, which is very expensive.

3 why doesn't the springboot startup class need the @ EnableTransactionManagement annotation

org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration

When spring.factories are loaded by SpringBoot, the transaction configuration class TransactionAutoConfiguration will be loaded. There is an internal configuration to enable transaction management.

// ~Internal class in TransactionAutoConfiguration
@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",
			matchIfMissing = 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 {

	}
}

4. Internal implementation mechanism of spring declarative transaction

When the application system calls to declare the target method of @ Transactional, the Spring Framework uses AOP proxy by default. Combined with AOP and transaction metadata (annotation), a proxy object is generated at code runtime. According to the attribute configuration information of @ Transactional, This proxy object determines whether the target method of the declared @ Transactional is intercepted by the interceptor TransactionInterceptor. When intercepted by the TransactionInterceptor, a transaction will be created and added before the target method starts to execute, and the logic of the target method will be executed. Finally, whether an exception occurs according to the execution, Use the abstract transaction manager AbstractPlatformTransactionManager to operate the data source to commit or roll back transactions.

The framework of transaction management is provided by the abstract transaction manager AbstractPlatformTransactionManager, and the specific underlying transaction processing implementation is implemented by the specific implementation classes of PlatformTransactionManager, such as the transaction manager DataSourceTransactionManager. Different transaction managers manage different data sources. For example, the DataSourceTransactionManager manages JDBC connections.

Reference documents

  1. https://www.cnblogs.com/xd502djj/p/10940627.html
  2. https://docs.spring.io/spring/docs/4.3.13.RELEASE/spring-framework-reference/htmlsingle/#transaction-declarative-annotations

Tags: Spring Boot Transaction

Posted on Sun, 21 Nov 2021 15:06:04 -0500 by hasek522