Personal summary of Spring transactions
1, Introduction
Although there have been a lot of information about Spring's transaction mechanism, it is often planted in the pit in actual use. I believe this is the experience or past experience of most programmers, so this article will make a complete summary, Use the way of combining theory with actual code practice to understand how the Spring transaction mechanism should be used and how it is designed internally.
Before introducing the main body, I hope this tutorial can not only avoid the trap of Spring transactions, but also exercise your architectural thinking from the transaction design of Spring. If you don't understand it at first, you can repeat it one by one like me on your own open source tools or projects, regardless of why you design it, Just imitate and think about it. If you think the article is well written, you are welcome to praise the collection and support a wave.
In addition, if you don't understand JDBC or database at all, this article may not be suitable for you. The author doesn't want to waste readers' time. You can look at this book written by a foreigner and roughly turn it over. It's still very suitable for getting started. By the way, hone your English reading level (it's really not good, and Google mother doesn't know the bottom):
jdbc (a simple and crude name)
Link: https://pan.baidu.com/s/1COVi...
Extraction code: x5tz
--Sharing from Baidu online disk super member V7
Best information
The best blog is always an official document. It is recommended to read it several times before understanding Spring transactions: (version 5.3.14)
https://docs.spring.io/spring...
In addition, the official document recommends two extension materials at the end of the transaction section, which are also recommended here:
For more information about transaction support for the Spring Framework, see:
Distributed transactions in Spring : a highly influential article on distributed transactions and a required reading to understand distributed transactions. A JavaWorld demonstration with and without XA. David Syer of Spring will guide you to understand seven modes of distributed transactions in Spring applications, three of which with XA and the other four without XA.
A translation is also found here: distributed-transactions-in-spring-with-and-without-xa.md
Java Transaction Design Strategies is a book provided by InfoQ, which provides a fast-paced introduction to transactions in Java. It also contains parallel examples of how to configure and use transactions through the Spring Framework and EJB 3.
Book resource link: https://pan.baidu.com/s/1Z1Vd... Extraction code: hads
2, Transaction introduction
First of all, we need to roughly understand what a transaction is. In a word, a group of operations are either executed together or not executed at all.
(1) Transaction attribute (ACID)
Transactions usually have the following four characteristics:
- Consistency: consistency ensures that the data before and after the operation of the transaction is correct.
- Atomicity: transaction is the smallest unit of operation. Each transaction either succeeds or fails.
- Isolation: isolation needs to ensure that different transactions will not interfere with each other.
- Persistence: after the transaction is committed, the data will be permanently effective and saved.
Although these four characteristics are simple, they are really difficult to remember. In order to facilitate memory, we use some cases for associative memory, or we can associate memory in a way that is easy to remember:
First of all, the data needs to ensure that the results before and after the operation are consistent. For example, if the bank card balance is 1000 yuan, we need to recharge 2000 into the bank card. After the recharge is completed, the data we see is 3000, otherwise it is still 1000 yuan. Therefore, the consistency needs to be guaranteed first. Then, assuming that the balance is still 1000 yuan, deduct 500 yuan and recharge 1000 yuan, If successful, the final balance must be 1500 yuan. It is necessary to ensure that the two operations are completed at the same time or not at the same time. This is atomicity. Atomicity can be regarded as ensuring the consistency of multiple continuous operations. The next step is isolation, which is easy to understand. If you recharge the ATM machine, only you can see all the data in the middle. If you deposit 1000 yuan, the result must be 2000 yuan instead of 500 yuan (you must call the police). Finally, transactions must be secure, so all transactions can take effect permanently as long as they succeed, and will not be affected by any sudden power failure. However, persistence depends on the implementation of specific databases. Of course, this is the main guarantee of most databases.
(2) Problems of concurrent transactions
There are several common problems with concurrent transactions, namely dirty reading, non repeatable reading, phantom reading, lost rollback (the first type of transaction is lost) and lost modification (the second type of transaction is lost). Here, we need to carefully distinguish between non repeatable and phantom reading.
1. Dirty reading: the simplest way to understand it is to read uncommitted data of other transactions, that is, uncommitted data. Therefore, whether the data is correct or not, the execution result may be incorrect.
2. Unrepeatable reading: unrepeatable reading reads the data modified or deleted by other transactions for the current transaction. For example, the data read behavior before the current transaction is 20, but the last read becomes 30. The data and the previous are not repeated, so it is unrepeatable reading. However, it should be noted that unrepeatable reading is not a very serious problem, This depends on the database, so this is one of the reasons why many databases do not set the default level to this level.
3. **Unreal reading**: The biggest difference between unreal reading and non repeatable reading is that other transactions are committed**newly added**Data, as if there is an illusion in reading data in the current transaction, and the content of the previously read value is inconsistent with that of the last read value. 3. **Lost modification**(The second type of transaction loss): there is a case of lost modification. Lost modification refers to the case where two transactions commit transactions to overwrite the same data at the same time, regardless of the transaction A Success or transaction B If successful, the data modified by one party will always be invalid. 3. **Loss rollback**(The first type of transaction (loss): loss rollback refers to A The transaction was committed, but B Transaction rolled back when A Submitted successfully, but B Because the transaction was rolled back A Data for A The transaction is lost.
(3) Isolation level of transaction
The isolation level of transactions is used to solve the problem of concurrent transactions. It should be noted that not all databases support the following contents (it may also support but only some). If there is no special description, it defaults to Mysql.
- READ UNCOMMITTED: corresponding to the dirty read mode, the uncommitted data of other transactions will be read, and there is almost no guarantee of data security and reliability.
- READ COMMITTED: transactions READ COMMITTED data, which is the default isolation level for most databases. However, when the submitted data is read, the new data submitted by other transactions can be read, so it can be read repeatedly.
- Repeatable read: this level is the default isolation level of MySQL. It solves the problem of dirty reading and ensures that the same records read by the same transaction multiple times are consistent, but phantom reading will still occur at this level. Phantom read means that when A transaction A reads data in A certain range, another transaction B inserts A row in this range, and transaction A reads data in this range again, A phantom row will be generated. Special note: InnoDB and XtraDB storage engines solve the phantom read problem through multi version concurrency control (MVCC). It uses next key locking to lock the rows involved in the query and the gaps in the index to prevent the insertion of phantom rows.
- SERIALIZABLE: this transaction is the highest isolation level. It forces the transaction to execute serially, avoiding the unreal read problem. In short, SERIALIZABLE will lock every row of data read, so it may lead to a lot of timeout and lock competition
(4) Set and rollback to savepoint
Note that the isolation level of savepoints is the same as that of transactions, which depends on whether the specific database supports it. Savepoints are used in the current transaction through connection Setsavepoint sets a Savepoint object using connection The rollback method rolls back and accepts a Savepoint parameter. It can be understood as an archive. After archiving, it can be rolled back to the specified archive point. Here, it is simply understood with an online sql case. The following case focuses on: ROLLBACK TO SAVEPOINT s1; And SAVEPOINT s1; And SAVEPOINT s2; These statements:
Rollback transaction to savepoint. --Delete table bonus_2017. DROP TABLE IF EXISTS bonus_2017; --Create table bonus_2017. CREATE TABLE bonus_2017(staff_id INT NOT NULL, staff_name CHAR(50), job VARCHAR(30), bonus NUMBER); --To table bonus_2017 in insert Record 1. INSERT INTO bonus_2017(staff_id, staff_name, job, bonus) VALUES(23,'limingwang','developer',5000); --Commit the transaction. COMMIT; --Set save point s1. SAVEPOINT s1; --To table bonus_2017 in insert Record 2. INSERT INTO bonus_2017(staff_id, staff_name, job, bonus) VALUES(24,'liyuyu','tester',7000); --Set save point s2. SAVEPOINT s2; --Query table bonus_2017 Data. SELECT * FROM bonus_2017; STAFF_ID STAFF_NAME JOB BONUS ------------ -------------------------------------------------- ------------------------------ ---------------------------------------- 23 limingwang developer 5000 24 liyuyu tester 7000 2 rows fetched. --Rollback to savepoint s1. ROLLBACK TO SAVEPOINT s1; --Query table bouns_2017 Data. SELECT * FROM bonus_2017; STAFF_ID STAFF_NAME JOB BONUS ------------ -------------------------------------------------- ------------------------------ ---------------------------------------- 23 limingwang developer 5000 1 rows fetched.
3, Introduction to Spring transactions
(1) What is a Spring transaction?
Spring transactions can be seen as a kind of encapsulation and cover up of the details of spring's operations on different databases. The whole transaction is controlled through three core components, which are:
- Platform transaction manager: (platform) transaction manager
- Transaction definition: transaction definition information (transaction isolation level, propagation behavior, timeout, read-only, rollback rules)
- TransactionStatus: transaction running status
During the operation of spring transactions, seven different transaction levels are customized to complete a series of operations such as transaction submission and rollback. Here is a key.
(2) Transaction design
Let's expand and introduce the contents of the above three components:
- PlatformTransactionManager: (platform) transaction manager, which defines the general interface of transactions. The main implementation abstract class is AbstractPlatformTransactionManager. As mentioned earlier, it defines the template method of transaction operation, and subclasses implement specific transaction details, such as rollback, transaction submission, parameter setting and so on.
Transaction definition: transaction definition information, which can be regarded as the attribute and configuration of a transaction connection. Transaction attributes are divided into the following five types:
- Transaction isolation level: not all databases support transaction isolation several times, and some databases may not support it due to different storage engines.
- Propagation behavior: there are seven propagation behaviors, and the default level is PROPAGATION_REQUIRED.
- Transaction timeout: refers to the maximum time allowed for the execution of a transaction. If the time limit is exceeded but the transaction has not been completed, the transaction will be rolled back automatically.
- Rollback rule: Specifies the circumstances under which the current transaction will be rolled back. By default, it is rolled back according to runtime exceptions. You can also set and specify that some exceptions will not be rolled back, but note that the specified exception class must be a subclass of Throwable.
- Read only: the read-only attribute of a transaction refers to read-only or read-write operations on transactional resources
- TransactionStatus: transaction running status, which can be regarded as a new transaction object. This interface contains the following contents
public interface TransactionStatus{ boolean isNewTransaction(); // Is it something new boolean hasSavepoint(); // Is there a recovery point void setRollbackOnly(); // Set to rollback only boolean isRollbackOnly(); // Rollback only boolean isCompleted; // Completed }
(3) Implementation form
There are two implementation forms of Spring transactions. The implementation forms will be explained in detail later:
- Programming mode: hard coding, also known as manual mode, can be regarded as a layer of encapsulation of jdbc. Generally, TransactionTemplate template template objects are used for transaction related operations.
- Declarative mode: it is characterized by using @ Transactional annotation to complete transaction injection of aspects. Its bottom layer uses aop and dynamic proxy.
Spring transaction is a textbook implementation of the programming ideas of aop and dynamic agent. If it is difficult to understand the spring source code, you can start from the spring transaction. If you don't understand it, you also need to supplement the lessons of aop and dynamic agent. Let's take a look at the implementation of programming transaction and declarative transaction respectively.
(4) Propagation level of the transaction
spring's transaction propagation level is mainly the following seven. Some details about transaction dissemination will be introduced in the "simultaneous interpreting transaction propagation level" of declarative transactions. At the same time, the actual combat will take a look at the effect of different communication levels through some simple cases. The specific case operation of transaction communication will be described in detail in the actual combat part.
characteristic | Propagation level | explain |
---|---|---|
Current transaction support: | TransactionDefinition.PROPAGATION_REQUIRED | If a transaction currently exists, join the transaction; If there is no current transaction, a new transaction is created. |
Current transaction support: | TransactionDefinition.PROPAGATION_SUPPORTS | If a transaction currently exists, join the transaction; If there are currently no transactions, continue to run in a non transactional manner. |
Current transaction support: | TransactionDefinition.PROPAGATION_MANDATORY | If a transaction currently exists, join the transaction; Throw an exception if there is no current transaction. (mandatory: mandatory) |
The current transaction is not supported: | TransactionDefinition.PROPAGATION_REQUIRES_NEW | Create a new transaction. If there is a current transaction, suspend the current transaction. |
The current transaction is not supported: | TransactionDefinition.PROPAGATION_NOT_SUPPORTED | Run in non transactional mode. If there is a transaction currently, suspend the current transaction. |
The current transaction is not supported: | TransactionDefinition.PROPAGATION_NEVER | Run in non transactional mode, and throw an exception if there is a transaction currently. |
Other situations | TransactionDefinition.PROPAGATION_NESTED | If a transaction currently exists, create a transaction to run as a nested transaction of the current transaction; If there is no transaction at present, this value is equivalent to transactiondefinition PROPAGATION_ REQUIRED. |
(5) Differences between Spring transactions and traditional
The differences between Spring transactions and traditional transactions are as follows:
- We don't need to catch SQLException. Spring will automatically catch exceptions and turn them into related runtimeexceptions and roll them back.
- Through Spring transaction management, configuration can be minimized, and transactions can be delegated to Spring for management.
- Traditional transactions will produce a lot of intrusive code, and switching databases is also a devastating blow to the system.
(6) Design idea (unimportant)
The design idea part is some thinking supplement after reading Spring transactions, which can be skipped.
The above briefly introduces the general characteristics of spring transactions, the characteristics of traditional transactions and the problems of concurrent transactions. Next, let's take a look at how spring designs its own transactions. Here, readers can stop and think about how you would design if you were a transaction designer? Note that this will help you further understand and consolidate the relevant knowledge about spring transactions. Here are some personal thoughts:
First of all, from the operational level, according to the characteristics of transactions, since transactions are multiple continuous atomic operations, we must start and commit transactions. Then, we must set some self-set attributes for transactions in the middle of the process, and use these attributes to control whether transactions are read-only, not allowed to be modified, whether transactions are allowed to be used, etc, Then we need to consider how to access unused databases, that is, how to manage data sources? At this time, you will definitely want to distribute to different data source transaction managers for control, but at this time, you will find that these methods, whether rolling back or committing transactions, are common, but the implementation methods are different. Therefore, you can use the policy mode to distribute transaction operations to specific data source implementations, However, because there are common methods, we need to use the pattern of template method to control the abstraction and different details of template method. Finally, the most important thing is how to roll back nested transactions and keep the source threads isolated during multi-threaded access? The most common thing we think of here is threadload, so finally, from the top-level abstraction, we only need three operations to complete a transaction: 1 Get transactions according to configuration, 2 Performing operations, 3 Commit or rollback the transaction.
Of course, Spring's transaction design is certainly much more complex than this, and has a more perfect architecture, but before learning the source code, the brain must have its own set of ideas, even if it is wrong, it doesn't matter. Calm down to see how the designer thinks and correct it according to his own ideas, otherwise it will be easily led by the details of the code.
4, Programming transaction
Programming transactions are only understood as the underlying implementation of declarative transactions. Now we basically do not use programming transactions directly. What we need to focus on is the core transaction component transaction manager.
(1) Implementation mode
Let's talk about the implementation of programmatic transactions. There are usually two implementation methods in the code:
- The first is to use the template class of TransactionTemplate for related operations.
- The second is to use platform transaction manager to control transaction processing.
So which method is more commonly used? In fact, both methods are not recommended because declarative transactions are more convenient than them, but programmatic transactions are the basis for the implementation of declarative transactions, so we have to talk about how they are implemented.
(2) Configuration mode
The configuration method generally doesn't need much memory, but sometimes wonderful interviewers will ask you if you have actually used it. In fact, most of the cases are unfamiliar, so here is a general template:
<!-- to configure c3po Connection pool --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!-- Injection attribute value --> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/xxxxx"></property> <property name="user" value="xxx"></property> <property name="password" value="xxxxx"></property> </bean> <!-- Programming transaction management --> <!-- Configure transaction manager, must be PlatformTransactionManager Supported implementation classes --> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- injection dataSource --> <property name="dataSource" ref="dataSource"></property> </bean> <!-- Configure transaction manager templates --> <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <!-- Inject a transaction manager that actually manages transactions,name Must be transactionManager Otherwise, it cannot be injected --> <property name="transactionManager" ref="dataSourceTransactionManager"></property> </bean>
Tip: note the difference between declarative configuration and declarative configuration.
(3)PlatformTransactionManager
Integration mode
The fastest way here is to build a Spring boot web project for introduction and direct use, because after version 4.2, the SpringBoot project can add transaction support without any annotation (default configuration). If you don't know how to operate, first we introduce the following dependencies in Maven:
You can refer to the following blog to quickly build a SpringBoot project:
Spring Boot quick build project - Zhihu (zhihu.com)
<!-- MYSQL --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- Spring Boot JDBC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
In order to observe the operation of transactions, an experiment table is constructed here:
CREATE TABLE `admin` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) DEFAULT NULL, `password` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3;
Quickly write a unit test to verify that the transaction is valid:
// unit testing class TransactionTemplateTest(){ @Autowired private ProgrammaticTrasaction programmaticTrasaction; /** * Transaction processing using TransactionTemplate */ @Test public void transactionTemplateTest(){ programmaticTrasaction.insert(); } } @Component public class ProgrammaticTrasaction { // Abnormal trigger switch private static final boolean EXCEPTION_FLAG = true; @Autowired private JdbcTemplate jdbcTemplate; @Autowired private TransactionTemplate transactionTemplate; public void insert(){ // 1. Build a transaction manager and set it using the data source of the system PlatformTransactionManager platformTransactionManager = new DataSourceTransactionManager(Objects.requireNonNull(jdbcTemplate.getDataSource())); // 2. Configure transaction attributes, such as transaction isolation level, transaction timeout and isolation method TransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); // 3. Start the transaction and call platform transaction manager through the default transaction attribute configuration Gettransaction starts the transaction operation, and the transaction status is obtained at this time TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition); // 4. Execute transaction related operations. Add is used here System.err.println("Query data list"); try { System.err.println(jdbcTemplate.queryForList("select * from admin")); // Note that the JDBC template unifies all data change operations as update operations, and the query starts with query jdbcTemplate.update("insert into admin (id, username, password) values (51, 'Lao Wang', '123')"); jdbcTemplate.update("insert into admin (id, username, password) values (21, 'Lao Zhang', '222')"); // 5. Submission of services platformTransactionManager.commit(transaction); // 6. Manually throw an exception for rollback //if(EXCEPTION_FLAG){ // throw new RuntimeException("throw exception manually"); //} } catch (Exception e) { // 6. We need to use the rollback method of the transaction manager to rollback the whole transaction System.err.println("Exception: trigger transaction rollback"); platformTransactionManager.rollback(transaction); } System.err.println("After inserting data"); System.err.println(jdbcTemplate.queryForList("select * from admin")); }/*Operation results: First execution Query data list [{id=1, username=admin, password=123456}] After inserting data [{id=1, username=admin, password=123456}, {id=21, username=Lao Zhang, password=222}, {id=51, username = Lao Wang, password=123}] Second execution: Query data list [{id=1, username=admin, password=123456}, {id=21, username=Lao Zhang, password=222}, {id=51, username = Lao Wang, password=123}] Trigger transaction rollback After inserting data [{id=1, username=admin, password=123456}, {id=21, username=Lao Zhang, password=222}, {id=51, username = Lao Wang, password=123}] */ }
The above code looks no different from the operation of writing template code. In fact, spring has done a lot of preparation and tedious basic work for us. Let's analyze the core code of spring transactions.
Source code analysis
The following is an interface diagram about programmatic transactions found on the network, which vividly illustrates the overall design of Spring transactions. Subsequent declarative transactions and various template operation classes are actually extensions based on the following structure:
PlatformTransactionManager transaction manager
First, let's take a look at the structure diagram of the transaction manager interface. It should be noted that the TransactionManager interface is a marked interface, which is marked as the transaction management interface of spring, All other objects must implement this tag interface to intervene in spring transactions (of course, it usually implements the platform transaction manager interface, or extends the abstract default transaction manager Implementation).
Tip: class diagrams of different spring versions may be different
The following is the PlatformTransactionManager transaction manager interface, which is also the core interface of spring transaction management mechanism.
public interface PlatformTransactionManager extends TransactionManager { /** Start a new transaction */ TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException; /** Commit a transaction */ void commit(TransactionStatus status) throws TransactionException; /** Transaction rollback */ void rollback(TransactionStatus status) throws TransactionException; }
As can be seen from the class diagram, there are many implementation classes here. Here we will introduce some common classes:
- jta transaction manager: jta transaction manager inherits from an abstract transaction management implementation. If you need to use distributed transaction management for operation, you can use this transaction manager. Of course, it is not useful in most cases.
Tips:
What is JPA? JPA itself is a specification. The products implemented based on this specification include Hibernate, eclipse link, TopLink, spring data, JPA, etc. the most typical implementation is Hibernate, the ancestor of ORM. If you want to learn more about JPA, check out this guest post: What is JPA? Introduction to Java Persistence API.
What is JTA? Be careful not to get confused with JPA. JTA refers to allowing applications to perform distributed transaction management, access and update data on two or more network computer resources. The addition of JDBC Driver JTA support greatly enhances the data access ability. If you know the details and specifications of JTA, you can see the blog at the beginning of the article, which will not be introduced here.
- Data source transaction manager: if you use a data source for operations, such as JDBC template and mybatis, you need to use this transaction manager.
- Ccilocal transaction manager: CCI (Common Client Interface) is the interface used by applications to interact with connectors and communicate with EIS. It also provides API s for local transaction demarcation. Spring provides CCI support mainly for common resources and transaction management tools at the same price, but it is rarely used.
How does Spring get transactions?
How can Spring get transactions from different data sources? The core lies in the AbstractPlatformTransactionManager class, a default implementation of the transaction manager, which implements the abstract logic of obtaining transactions by default and distributes the specific acquisition logic to. The following is the method org springframework. transaction. support. Execution process of AbstractPlatformTransactionManager#gettransaction method:
- Check whether the transaction attribute TransactionDefinition is passed. If not, the default configuration is used.
- Take the DataSourceTransactionManager data source as an example, build a transaction object through the DataSourceTransactionManager#doGetTransaction() method, set relevant properties, and synchronously obtain the ConnectionHolder (connector) object through TransactionSynchronizationManager#getResource(), transactionsynchronizationmanager The getresource () method is key. Spring uniformly configures the connector in a NamedThreadLocal global thread shared resource. At the same time, since the key may be a dynamic proxy object, the key acquisition will try to unpack the dynamic proxy to obtain the actual target object or directly use the transparent proxy, When the connector configured in NamedThreadLocal is obtained, it will be returned (at this time, it will be additionally checked whether the current thread still exists, and the connector exists in the global variable. If it does not exist, the current thread resources will be removed to free memory).
- After that, the code logic is relatively simple. Judge the transaction propagation mechanism through the transaction attribute object TransactionDefinition, and then the familiar template code for a series of operations such as starting a transaction, executing sql and returning results. Here, the design mode of policy and template is combined, and the code is easy to understand.
- The last step of committing a transaction is special. If the current connection is found to be a new connection after the transaction is committed, the TransactionSynchronizationManager#bindResource will be called to bind the current connector, which can ensure that the same transaction is propagated in the same thread.
The following is the abstract method of obtaining transaction, and the following is the details of the method of obtaining transaction:
@Override public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { // Check whether the transaction attribute TransactionDefinition is passed. If not, the default configuration is used. TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults()); // To obtain a transaction from a specific data source, the most common is Object transaction = doGetTransaction(); boolean debugEnabled = logger.isDebugEnabled(); if (isExistingTransaction(transaction)) { // Check whether a transaction already exists, judge according to the propagation level of the transaction, and create a new transaction or throw an exception return handleExistingTransaction(def, transaction, debugEnabled); } // Check whether the transaction timeout is set if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout()); } // If no transaction is found, check whether the transaction propagation level is abnormal 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) { SuspendedResourcesHolder suspendedResources = suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def); } try { // Open transaction return startTransaction(def, transaction, debugEnabled, suspendedResources); } catch (RuntimeException | Error ex) { resume(null, suspendedResources); throw ex; } } else { // Create an "empty" transaction: there is no actual transaction, but it may be synchronized. if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) { logger.warn("Custom isolation level specified but no actual transaction initiated; " + "isolation level will effectively be ignored: " + def); } boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null); } }
Finally, we simplify the above description to pseudo code for explanation:
DataSource transaction object, representing a ConnectionHolder, is used as the transaction object of DataSourceTransactionManager.
Tip: if Springboot does not configure a data source, it will use a data source called HikariPool by default. This configuration is the default value after 2.0.
// Bind a globally shared global variable using threadlocal private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources"); DataSource dataSource = platformTransactionManager.getDataSource(); // Get details for the transaction manager below = = = = > // Get the resource pool corresponding to the current thread from the global shared variable Map map = resources.get(); // Actualkey = > is the actual object (aop is the actual object) ConnectionHolder localConnectionHolder = resources.get(actualKey, map.get(actualKey)); if(map.isEmpty()){ // Additionally, check whether the current thread still exists. The connector exists in the global variable. If it does not exist, remove the current thread resource release to avoid the released Thread objects piling up in the shared resources resources.remove(); } //Set up manual commit transactions localConnectionHolder.setAutoCommit(false); map.put(datasource,connection); resources.set(map); // <==========
Once again, the core part of the whole spring transaction is AbstractPlatformTransactionManager, which defines most of the common logic for spring to obtain transactions, and the specific details about acquisition, commit, rollback and so on need to be implemented by the specific transaction manager of the subclass.
Finally, programming transactions are still very common in the past (because there are no other tools). Spring's annotated declarative transactions are easy to use, but they gradually fade out of sight. However, learning these contents will help to better understand how spring implements annotated transactions, because the core implementation is still completed through the transaction manager.
Tip: I suggest you take a break, tidy up your thoughts, go out and pour a glass of water, come back and watch it slowly.
(4)TransactionTemplate
Now let's look at the second method, TransactionTemplate. The second method is actually a further encapsulation of the pure manual form. We can find that the template code of the transaction manager can actually save the steps of starting a transaction, submitting a transaction and rolling back. There will still be a lot of template code in these steps, In addition, the client does not like to manually open and close transactions. Spring improves this by creating a template method class for TransactionTemplate transactions. Let's explain its usage according to an actual running code:
Mode of use
public void update(){ // 1.2 instead of using the transaction manager, it is directly integrated within the transactionTemplate // 3. The difference is that the transactionTemplate is used to start and control transactions. The following two main usage methods are introduced // 3.1 transactionTemplate.executeWithoutResult(Consumer<TransactionStatus> action): // If there is no return value, you need to pass a Consumer object to do business operations in the accept method. (similar section) // 3.2 < T > t execute (TransactionCallback < T > action): if there is a return value, you need to pass a TransactionCallback object to do business operations in doInTransaction method // After calling the execute method or the executeWithoutResult method, the transaction manager will automatically commit the transaction or roll back the transaction. // Under what circumstances will a transaction be rolled back? The following conditions need to be met: // (1)transactionStatus.setRollbackOnly(); Mark the transaction status as rollback status // (2) An exception is thrown inside the execute method or executeWithoutResult method // When will the transaction be committed? // Method has no exception & & transactionstatus has not been called setRollbackOnly(); System.err.println("Before transaction"); System.err.println(jdbcTemplate.queryForList("select * from admin")); // The first way to use transactionTemplate.executeWithoutResult(transactionStatus -> { jdbcTemplate.update("update admin set username ='test2' where id = '51'"); jdbcTemplate.update("update admin set username ='test2',password='1111' where id = '21'"); }); // The second way to use DefaultTransactionStatus execute = (DefaultTransactionStatus) transactionTemplate.execute((TransactionCallback<Object>) status -> { jdbcTemplate.update("update admin set username ='test2' where id = '51'"); jdbcTemplate.update("update admin set username ='test2',password='1111' where id = '21'"); return status; }); System.err.println("Post transaction"); System.err.println(jdbcTemplate.queryForList("select * from admin")); }/*Operation results: Before transaction [{id=1, username=admin, password=123456}, {id=21, username=Lao Zhang, password=222}, {id=51, username = Lao Wang, password=123}] Post transaction [{id=1, username=admin, password=123456}, {id=21, username=test2, password=1111}, {id=51, username=test2, password=123}] */
It can be seen that the use of TransactionTemplate reduces a lot of code. We don't need to care about the details of transaction startup and submission, but only about the transaction processing operations.
Source code analysis
Let's explain the general processing flow of this operation according to the same source code. It may feel that I wipe so miraculously. I don't need a transaction manager or set properties. How do I set properties? In fact, all the work is just handed over to the TransactionTemplate. In order to verify, we can compare the methods in DefaultTransactionDefinition and TransactionTemplate and find that their methods are basically the same. (in fact, the TransactionTemplate inherits the DefaultTransactionDefinition.). In most cases, we inject beans through @ Configuration to set some default configurations for the global TransactionTemplate.
In fact, from a personal point of view, it is not very good to use inheritance to implement spring related transaction configuration here. Instead, it is more appropriate to use a configuration constructor to complete the transfer of configuration or to use a combined TransactionDefinition.
DefaultTransactionDefinition :
TransactionTemplate :
Next, let's take a look at its class diagram. We can see that it implements a new interface based on extending the transaction attribute class: transactionoperation, which is also a key interface. In addition, the reason why TransactionTemplate can perform various transaction operations is that it internally combines the objects of PlatformTransactionManager, Therefore, all transaction management is actually managed through the PlatformTransactionManager object
The following is the method signature of TransactionOperations. Note that spring 5 has made changes and extensions to this interface, and the package contents of the old version may be different:
class TransactionOperations{ /** Method with return value */ @Nullable <T> T execute(TransactionCallback<T> action) throws TransactionException; /** jdk8 The default method of the interface without return value @since: 5.2 */ default void executeWithoutResult(Consumer<TransactionStatus> action) throws TransactionException { execute(status -> { action.accept(status); return null; }); } /** Returns the implementation of the TransactionOperations interface, which executes the given TransactionCallback without an actual transaction. Usually used during testing: this behavior is equivalent to running with the transaction manager without actual transactions (deployment_supports) and synchronization_never. For TransactionOperations implementations with actual transactions, use the TransactionTemplate with the appropriate platform transactionmanager. @since: 5.2 @see: TransactionDefinition.PROPAGATION_SUPPORTS, AbstractPlatformTransactionManager.SYNCHRONIZATION_NEVER, TransactionTemplate */ static TransactionOperations withoutTransaction() { return WithoutTransactionOperations.INSTANCE; } }
Let's take another look at the TransactionTemplate. The code is relatively simple. I believe readers can understand it as long as they understand the original transaction operation template methods:
/** explain: Performs the operation specified by the given callback object in a transaction. Allows you to return the result object created in the transaction, That is, a domain object or a collection of domain objects. The RuntimeException thrown by the callback is regarded as an exception for forced rollback. Such exceptions are propagated to the caller of the template. Parameters: action - Specifies the callback object for the transaction operation return: The result object returned by the callback, if not, is null Throw: TransactionException - In case of initialization, rollback or system error RuntimeException - Thrown by TransactionCallback */ @Override @Nullable public <T> T execute(TransactionCallback<T> action) throws TransactionException { // Check whether a transaction manager currently exists (cannot be empty) Assert.state(this.transactionManager != null, "No PlatformTransactionManager set"); // If the current transaction manager supports automatic callback processing, use the implementation of the automatic callback method of the transaction manager. The CallbackPreferringPlatformTransactionManager class extends from PlatformTransactionManager and exposes the method of executing a given callback in the transaction. if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) { return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action); } else { // If automatic callback is not supported, you will switch to the traditional processing mode, which is similar to the original transaction management operation TransactionStatus status = this.transactionManager.getTransaction(this); T result; try { // The core part uses the functional call of action to realize the internal transaction processing result = action.doInTransaction(status); } catch (RuntimeException | Error ex) { // How to generate RuntimeException or error rollback rollbackOnException(status, ex); throw ex; } catch (Throwable ex) { // Exceptions are rolled back according to the transaction level rollbackOnException(status, ex); throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception"); } // If there is no exception, submit normally this.transactionManager.commit(status); // Returns the result of transaction execution return result; } }
After understanding the execute method with return value, let's take a look at the method executeWithoutResult() without return value. Its implementation code is more concise. It can be seen from the code of the latest version that functional expression reconstruction is performed in subsequent versions. Therefore, incompatibility may occur if the jdk version is relatively low:
/** Performs the operation specified by the given Runnable in the transaction. If you need to return an object from a callback or access TransactionStatus from a callback, you can use execute(TransactionCallback) instead. Such variants are similar to using TransactionCallbackWithoutResult, but have simplified signatures for common situations - and can be easily used with Java 8 lambda expressions. Parameters: action - Specifies the Runnable of the transaction operation Throw: TransactionException - In case of initialization, rollback or system error RuntimeException - If thrown by Runnable */ default void executeWithoutResult(Consumer<TransactionStatus> action) throws TransactionException { execute(status -> { action.accept(status); return null; }); }
(5) Summary
Finally, let's summarize the above contents. At the beginning of this section, we learned about the operation of traditional programmatic transactions. We can see that although Spring has encapsulated the details of opening and closing transactions and the template code for JDBC operations, if we use programmatic transactions, we will still generate a lot of intrusive code, This is also inconsistent with the principle of single responsibility.
Several features of programming transaction implementation:
- The transaction propagation mechanism is based on the AbstractPlatformTransactionManager#getTransaction() method and manages transactions through different transaction propagation level settings.
- In order to ensure the consistency of connection resources used by the same thread, Spring uses ThreadLocal as the storage of global shared variables to realize transaction isolation between threads and prevent the problem of concurrent transactions.
- The TransactionTemplate can be regarded as a secondary encapsulation of the platform transactionmanager transaction manager and extends the callback type transaction operation.
Again, it's not recommended to spend a lot of time on the introduction, but the use of programmatic transactions is more helpful for us to understand the use of declarative transactions. Let's learn about the use of declarative transactions.
5, Declarative transaction
The use of declarative transactions is very simple and easy to understand, that is, the @ Transactional annotation we are familiar with, which does not invade the addition and removal of transaction management support. The implementation of declarative transactions uses AOP. In fact, the essence is to intercept before executing the annotated transaction methods. Declarative transactions have the following advantages and disadvantages:
Advantages: simple and fast transaction injection without changing any business code.
Disadvantages: consistent with the disadvantages of spring aop, transaction control can only be carried out at the method level, and declarative transactions can only be used through Spring proxy objects. If you access transactions with new objects, it will not take effect (remember) and has certain limitations.
(1) How to use?
Different projects are configured in different ways and need to be used for specific project environment. At present, the mainstream is to use microservices. In fact, it is more recommended to use @ Bean class configuration to inject relevant functions of transaction management, such as configuring transaction manager. If you are a SpringBoot and the version is relatively new, it is very simple. You can directly annotate it.
Tip: if you are using spring boot 2 For projects after 0, and if there are dependencies related to spring data - * or spring TX transactions in the dependencies, declarative transactions will be enabled by default.
@Override @Transactional(rollbackFor = Exception.class) public void testAdd() { //dosomething }
However, if the Spring Boot version is relatively low or some older projects, you need to manually start it:
@Configuration @EnableTransactionManagement public class MySpringConfig { @Bean public PlatformTransactionManager txManager() { // dosomething can configure some transaction managers by itself. return yourTxManager; } }
Incidentally, if we want to use the methods in the programmatic TransactionAspectSupport class in the annotated methods to manually rollback transactions, we can use the following methods to rollback (but it is not recommended):
public void createCourseDefaultRatingProgramatic(Course course) { try { courseDao.create(course); } catch (Exception e) { // Manual rollback TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } }
@Introduction to Transactional annotation
Let's take a look at what configurations @ Tralsactional has:
- value: alias, specifying the Bean of the transaction manager to be used
- Transaction manager: Specifies the qualifier value of the transaction. Can be used to determine the target transaction manager to match the qualifier value (or bean name) defined by a specific TransactionManager bean.
- Propagation: used to specify the propagation level, core and common attributes of transactions
- Isolation: the isolation level of transactions. If it is not specially specified, it will be based on the database level by default
- Timeout: represents the timeout of the current transaction (in seconds). It is the default timeout of the underlying transaction system by default (note that the code gives a default value of 1 second). Designed for communication with propagation Required or propagation REQUIRES_ It is designed for use with new. It should be noted that it is only applicable to newly started transactions.
- readOnly: the default value is false. If it is set to true, only non update operations will be executed. If an update operation is encountered, an exception will be thrown. This configuration method helps to improve transaction execution efficiency. If update related transactions are not required, you can set this to true.
- rollbackFor: rolls back according to the specified Class exception Class. If it is not set, it is the runtime exception by default, but it must be a subclass of able.
- rollbackForClassName: must be a substring of the fully qualified class name of a subclass of able. Wildcards are not supported at present. For example, the value of ServletException will match javax servlet. ServletException and its subclasses.
- noRollbackFor: the opposite of rollbackFor.
- noRollbackForClassName: the opposite of rollbackForClassName.
If the same annotation is used in multiple places, but we don't want to always copy and paste a large number of duplicate annotation configurations, one way is to use custom annotation. Here's a simple understanding.
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Transactional(transactionManager = "order", label = "causal-consistency") public @interface OrderTx { } @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Transactional(transactionManager = "account", label = "retryable") public @interface AccountTx { }
Listener binding event
Listener transaction binding events require spring 4 2 or above. Spring 4 is required 2 or above. Spring 4 is required It's very important to support more than 2, so it's specially emphasized three times. Typically, a general event listener is registered using the @ EventListener annotation. If you need to bind it to a transaction, you can also use @ TransactionalEventListener to implement the transaction operation of the listener.
The following is an official case:
@Component public class MyComponent { @TransactionalEventListener public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) { // ... } }
The @ TransactionalEventListener annotation is familiar with the phase attribute:
TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;
This attribute supports transaction binding to the specified phase, including the following levels:
- BEFORE_COMMIT
- AFTER_COMMIT (default)
- AFTER_ROLLBACK
- AFTER_COMPLETION
The scenario of listener using transactions has not been encountered by individuals at present, so here is only a brief translation and introduction according to the official documents.
JTA integration
If JTA needs to be integrated, the configuration file cannot use a specific resource, but needs to cooperate with JNDI and jtatatransactionmanager transaction manager for global transaction management. Let's take a look at the configuration method.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jee https://www.springframework.org/schema/jee/spring-jee.xsd"> <jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/> <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" /> <!-- other <bean/> definitions here --> </beans>
Integration with JPA/Hibernate
Be careful not to be dazzled. The previous section talks about JTA. This section talks about ORM related JPA. Their concepts will not be repeated here. If your database uses JPA, using declarative transactions can avoid a core problem. The following is a code case:
public class UserService { @Autowired private SessionFactory sessionFactory; // (1) public void registerUser(User user) { // Open connection Session session = sessionFactory.openSession(); // (2) // Start a JPA transaction session.beginTransaction(); // Perform an insert operation session.save(user); // Commit transaction session.getTransaction().commit(); // Close connection session.close(); } }
There are two serious problems with such code:
- Hibernate doesn't know the @ Transactional annotation of Spring.
- Spring's @ Transactional knows nothing about Hibernate transactions.
If we use declarative transactions, the above situation will not occur. Spring will automatically switch to when the transaction starts HibernateTransactionManager or JpaTransactionManager (if Hibernate is used through JPA), Hibernate transaction manager will ensure that the two session forms are merged into the same, which also reflects a very common idea of adapter mode.
Responsive transaction
At present, responsive transaction is still a relatively novel thing and the main direction of Spring in the future. The idea of extreme programming is more interesting, but from the current environment, the frequency of use is relatively low, so this article will not expand.
For more integration and usage methods, please refer to the official documents. The article is limited in length and can't cover everything.
(2) Configuration mode
There are four ways to configure declarative transactions, of which annotation is the most commonly used way for transaction processing.
- TransactionInterceptor: the method of using TransactionInterceptor interceptor. Although this method is not recommended, it is helpful to understand spring transactions. If you are interested, you can read this class.
- AspectJ Based XML method (\ < TX \ > and \ < aop \ > tags): you do not need to change the class. You can configure it in the XML file. For example, the following provides a transaction configuration template, which is also used to intercept and increase transactions based on aspect aop.
<!-- Transaction manager --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- data source --> <property name="dataSource" ref="dataSource" /> </bean> <tx:annotation-driven transaction-manager="transactionManager" /> <!-- notice --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- Communication behavior --> <tx:method name="save*" propagation="REQUIRED" /> <tx:method name="insert*" propagation="REQUIRED" /> <tx:method name="delete*" propagation="REQUIRED" /> <tx:method name="update*" propagation="REQUIRED" /> <tx:method name="find*" propagation="SUPPORTS" read-only="true" /> <tx:method name="get*" propagation="SUPPORTS" read-only="true" /> </tx:attributes> </tx:advice> <!-- section --> <aop:config> <aop:advisor advice-ref="txAdvice" pointcut="execution(* service.*.*(..))" /> </aop:config>
- Configure based on the Bean of the transaction proxy factory: for example, the following Bean is configured with a proxy factory - transactionproxyfactorybean. This method was previously recommended by Spring, and the configuration method is also relatively old, and it was as early as Spring 2 0 is not recommended.
..... <!-- Transaction manager --> <bean id="myTracnsactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- Transaction agent factory --> <!-- Generate transaction proxy object --> <bean id="serviceProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager" ref="myTracnsactionManager"></property> <property name="target" ref="buyStockService"></property> <property name="transactionAttributes"> <props> <!-- main key Is the method ISOLATION_DEFAULT Isolation level of transaction PROPAGATION_REQUIRED Communication behavior --> <prop key="add*">ISOLATION_DEFAULT,PROPAGATION_REQUIRED</prop> <!-- -Exception Indicates that the specified exception rollback occurred,+Exception Indicates that the specified exception submission occurred --> <prop key="buyStock">ISOLATION_DEFAULT,PROPAGATION_REQUIRED,-BuyStockException</prop> </props> </property> </bean> .....
- Finally, it is common to use @ Transactional annotation for configuration. If SpringBoot can directly introduce transactions with @ EnableTransactionManagement, @ Transactional can be configured on a class or on a method, but it also depends on AOP. At the same time, it only supports method level transaction control rather than code block level transaction control.
@Transactional(isolation=Isolation.DEFAULT,propagation=Propagation.REQUIRED,rollbackFor=BuyStockException.class)
(3) Understanding transaction propagation levels
We mentioned the judgment of transaction propagation mechanism through the transaction attribute object TransactionDefinition. Here, we will take a look at the type of transaction propagation mechanism. The propagation level of transaction has been mentioned earlier. Here, we will review:
characteristic | Propagation level | explain |
---|---|---|
Current transaction support: | TransactionDefinition.PROPAGATION_REQUIRED | If a transaction currently exists, join the transaction; If there is no current transaction, a new transaction is created. |
Current transaction support: | TransactionDefinition.PROPAGATION_SUPPORTS | If a transaction currently exists, join the transaction; If there are currently no transactions, continue to run in a non transactional manner. |
Current transaction support: | TransactionDefinition.PROPAGATION_MANDATORY | If a transaction currently exists, join the transaction; Throw an exception if there is no current transaction. (mandatory: mandatory) |
The current transaction is not supported: | TransactionDefinition.PROPAGATION_REQUIRES_NEW | Create a new transaction. If there is a current transaction, suspend the current transaction. |
The current transaction is not supported: | TransactionDefinition.PROPAGATION_NOT_SUPPORTED | Run in non transactional mode. If there is a transaction currently, suspend the current transaction. |
The current transaction is not supported: | TransactionDefinition.PROPAGATION_NEVER | Run in non transactional mode, and throw an exception if there is a transaction currently. |
Other situations | TransactionDefinition.PROPAGATION_NESTED | If a transaction currently exists, create a transaction to run as a nested transaction of the current transaction; If there is no transaction at present, this value is equivalent to transactiondefinition PROPAGATION_ REQUIRED. |
Understanding promotion_ REQUIRED:
PROPAGATION_REQUIRED if there is no transaction, a new transaction will be created. If there is a nested transaction, it will participate in the outer transaction instead of creating a new transaction. This is a very convenient default setting for public calls in the same thread (for example, the service appearance delegated to multiple repository methods, in which all underlying resources must participate in service level transactions). At the same time, it can basically deal with most business systems.
Discussion: let's discuss the "conflict" between the official Spring documentation and the source code description. By default, if nested transactions occur, the existing transactions will be adopted by default. In this way, the transaction isolation level, timeout value or read-only flag (if any) will be ignored by default for internal new transactions. If you want to be rejected by external transactions and throw exceptions when internal transactions adopt different isolation levels, consider switching the validateExistingTransactions flag on the transaction manager to true.
In fact, this statement is not particularly consistent with the source code. In fact, if validateExistingTransactions is set to false, it will be found that the transaction will continue to be executed. Even if the isolation level is different from the request, it will be executed according to the priority of external transactions. In fact, the transaction configuration only depends on one file, For details, please refer to the following code:
if (isValidateExistingTransaction()) { if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { I } // If read-only is set but the current transaction is not read-only, an exception is thrown if (!definition.isReadOnly()) { if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { throw new IllegalTransactionStateException("Participating transaction with definition [" + definition + "] is not marked as read-only but existing transaction is"); } } } // If not, build an empty transaction synchronously
Finally, add an official picture:
Understanding promotion_ REQUIRES_ NEW:
PROPAGATION_REQUIRES_NEW vs. promotion_ Required, always use separate transactions for each affected transaction scope, and never participate in existing transactions in the external scope. Each transaction resource is independent, so it can be committed or rolled back independently. External transactions are also not affected by the rollback state of internal transactions. The lock of internal transactions is released immediately after completion. Such an independent internal transaction can also declare its own isolation level, timeout and read-only settings without inheriting the characteristics of external transactions.
Understanding promotion_ NESTED:
PROPAGATION_NESTED uses a savepoint approach. Nested transactions mean that some transactions can be rolled back, but nested internal transactions will not affect the execution of external transactions. Note that this setting is only applicable to JDBC resource transactions. It is also explained in the source code. You can refer to Spring DataSourceTransactionManager.
(3) Source code analysis
Declarative transactions are much more complex than programmatic transactions. The code in this part will not explain the part of AOP in depth. At the same time, due to the use of the feature of SpringBoot automatic injection, we will try not to talk too deeply here. Let's analyze the declarative internal operation process from two parts:
- Auto configuration injection
- Annotation interception and parsing
Auto configuration injection
First, gnaw on the first big bone. Let's start with an annotation of SpringApplication. As mentioned above, if there are transaction dependencies in the new version of SpringBoot, it will automatically access Spring's transaction control. We can check through the annotation @ EnableTransaction and find that there is an internal @ EnableAutoConfiguration. When Spring starts, The root path of the source code package will be scanned. The one below is called Spring The factories file has an org. Org in about 127 lines springframework. boot. autoconfigure. transaction. The configuration of TransactionAutoConfiguration. Under this configuration, there is a configuration of TransactionAutoConfiguration. When the container is started, it will automatically scan relevant packages and load relevant bean s according to the configuration. Finally, from the annotations automatically injected by transactions, you can see some familiar shadows from the screenshot below, which will not be explained again here.
Next, let's look back at the internal content of the @ EnableTransaction annotation. Here @ import is used to inject the TransactionManagementConfigurationSelector. The function of this class is based on the enabletransactionmanagement. Import on the @ Configuration class The value of mode() selects which implementation of AbstractTransactionManagementConfiguration should be used.
@Import(TransactionManagementConfigurationSelector.class) public @interface EnableTransactionManagement
We can see from the following code that two proxy methods are provided here. One is based on the JDK proxy mode, and the other is based on the AspectJ proxy default. Spring will give priority to the JDK proxy by default. If not, it will choose the AspectJ proxy (I always think the spring author has a regret and obsession with Aop), If you want to know where to configure it, you can check out org springframework. scheduling. annotation. There is a default JDK proxy configuration in enableasync#mode. In the following code, you can inject two classes autoproxyregister into the JDK proxy Class and proxytransactionmanagementconfiguration class .
AdviceMode mode() default AdviceMode.PROXY; -----Split line---- /** Generate different agents according to the selected agent mode */ @Override protected String[] selectImports(AdviceMode adviceMode) { switch (adviceMode) { // The JDK agent will be used by default case PROXY: return new String[] {AutoProxyRegistrar.class.getName(), ProxyTransactionManagementConfiguration.class.getName()}; // If you select ApectJ, you can refer to the following code case ASPECTJ: return new String[] {determineTransactionAspectClass()}; default: return null; } }
AutoProxyRegistrar.class:
The literal translation of this class is the automatic agent registrar. It is a post processor, which is responsible for registering an infrastructure advisor autoproxycreator and infrastructure advisor autoproxycreator in the container. After the object is created, the post processor mechanism is used to wrap the object, return a proxy object (enhancer), and the proxy object executes the method, Use the interceptor chain to call. The following is some code about the Registrar:
@Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { boolean candidateFound = false; Set<String> annTypes = importingClassMetadata.getAnnotationTypes(); for (String annType : annTypes) { // Parsing and obtaining annotation information 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()) { // Verify whether the agent is set to be enabled. If configured, check whether it is a JDK agent candidateFound = true; if (mode == AdviceMode.PROXY) { // Core code, create an automatic proxy creator if necessary AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry); if ((Boolean) proxyTargetClass) { // Forces the automatic proxy creator to bind to the user agent AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); return; } } } } //...... }
Then enter the method AopConfigUtils registerAutoProxyCreatorIfNecessary(registry); Internal implementation org springframework. aop. config. AopConfigUtils #registerorexcalateapcasrequired method. AopConfigUtils is the main tool of AOP dynamic proxy class Creator:
@Nullable private static BeanDefinition registerOrEscalateApcAsRequired( Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) { // Assert whether there is a Bean. BeanDefinitionRegistry is the actual form of a Bean in the Spring container Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); // The Registrar first determines whether there is an injection of org springframework. aop. config. internalAutoProxyCreator if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); if (!cls.getName().equals(apcDefinition.getBeanClassName())) { // Here, it is obtained according to the priority. There must be no injection when it comes in for the first time int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); int requiredPriority = findPriorityForClass(cls); if (currentPriority < requiredPriority) { apcDefinition.setBeanClassName(cls.getName()); } } return null; } // It can be seen as packaging infrastructure advisor autoproxycreator into rootbean definition RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); beanDefinition.setSource(source); beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); // Core part: inject bean s into the ioc container. The name is org springframework. aop. config. internalAutoProxyCreator registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); return beanDefinition; }
After knowing how the class infrastructure advisor autoproxycreator is integrated into the IOC container, we find that this class belongs to a subordinate implementation subclass. We find the parent class through the inheritance chain: AbstractAutoProxyCreator. This class name is translated as an automatic proxy creator. Its function is if the bean is identified as a proxy by the subclass, Then the configured interceptor is used to create the agent, which can be regarded as the decoupling between the producer and executor of the agent. The core code is as follows:
@Override public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { // Is enhanced packaging necessary return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; }
The internal code logic is not much expanded. To sum up, the function of infrastructure advisor autoproxycreator is to wrap the object after the object is created by using the post processor mechanism, return a proxy object (enhancer), execute the method of the proxy object, and finally call it by using the interceptor chain.
Tips: this part of the code needs to have a firm grasp of the bottom layer of the implementation of Spring IOC and AOP. It is recommended not to be stubborn after the reading card is dead. You can supplement the implementation of other modules and then gnaw at how transactions are integrated into IOC and AOP.
Annotation interception and parsing
After we have a general understanding of how automatic configuration is injected into Spring IOC, let's take a look at how Spring implements annotation interception and parsing of transactions, that is, the parsing process of @ Transactional annotation. As mentioned above, the annotation interceptor call is no longer verbose here, Here we go back to the object ProxyTransactionManagementConfiguration mentioned earlier, that is, the agent configuration manager. Here, the reader may have questions. This class suddenly appears. Here is a hint:
case PROXY: return new String[] {AutoProxyRegistrar.class.getName(), ProxyTransactionManagementConfiguration.class.getName()};
Some codes are intercepted below:
@Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) { BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor(); // Inject an annotation parser into the aspect to parse the transaction annotations advisor.setTransactionAttributeSource(transactionAttributeSource); // The interceptor that injects transactions into the aspect is dedicated to intercepting methods, including transaction submission and rollback operations advisor.setAdvice(transactionInterceptor); if (this.enableTx != null) { advisor.setOrder(this.enableTx.<Integer>getNumber("order")); } return advisor; }
Next, let's take a look at the injected attribute: TransactionAttributeSource. This class is used to process the attributes and related contents of annotations. It is also an annotation parser. This class has many implementation subclasses because it is an annotation parser. Here we look directly at:
Some codes are as follows:
public AnnotationTransactionAttributeSource(boolean publicMethodsOnly) { this.publicMethodsOnly = publicMethodsOnly; if (jta12Present || ejb3Present) { this.annotationParsers = new LinkedHashSet<>(4); // spring annotation parser this.annotationParsers.add(new SpringTransactionAnnotationParser()); if (jta12Present) { // jta parsing start this.annotationParsers.add(new JtaTransactionAnnotationParser()); } if (ejb3Present) { // Parser for ejb3 this.annotationParsers.add(new Ejb3TransactionAnnotationParser()); } } else { // Using spring annotation parser this.annotationParsers = Collections.singleton(new SpringTransactionAnnotationParser()); } } //Call org springframework. transaction. annotation. The internal code of the springtransactionannotationparser#parsetransactionannotation method is as follows: public class SpringTransactionAnnotationParser implements TransactionAnnotationParser, Serializable { 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()); rbta.setReadOnly(attributes.getBoolean("readOnly")); rbta.setQualifier(attributes.getString("value")); 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); return rbta; } }
The above code is easy to understand, which is to parse and encapsulate the internal annotation. Next, let's take a look at the interceptor TransactionAspectSupport. This interceptor implements the MethodInterceptor interface, which is marked as a method interceptor, and the core method is the internal invoke() method.
The internal invoke() method is as follows:
@Override @Nullable public Object invoke(MethodInvocation invocation) throws Throwable { // Calculate the target class: it may be {@ code null}. TransactionAttributeSource should pass the target class and method, which may come from the interface. Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); // Adaptation method execution return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); }
Then go to the core adaptation method invokeWithinTransaction. There are many codes. Here is a partial code Description:
@Nullable protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable { // If the transaction attribute is empty, the method is non transactional (that is, executed in a non transactional manner) TransactionAttributeSource tas = getTransactionAttributeSource(); final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); final TransactionManager tm = determineTransactionManager(txAttr); // If the current registrar is responsive, it is handled in a responsive manner if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) { // ....... } PlatformTransactionManager ptm = asPlatformTransactionManager(tm); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { // Use getTransaction and commit/rollback calls to partition transactions. TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); Object retVal; try { // This is a wrap around notification that calls the next interceptor in the interception chain. The target object is actually called. retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // The target method call failed completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { // Subsequent cleanup of transactions cleanupTransactionInfo(txInfo); } // if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) { // If the rollback rule is met, rollback is performed TransactionStatus status = txInfo.getTransactionStatus(); if (status != null && txAttr != null) { retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status); } } // If no exception occurs and the transaction manager is obtained, the transaction is committed commitTransactionAfterReturning(txInfo); return retVal; } // .......
6, Spring transaction practice
(1) Introduction
In order to prevent readers from misunderstanding, the Spring version and database version of this experiment are introduced in advance:
- spring-boot: 5.2.8
- mysql: 5.7
Let's move on to the actual part. The actual part mainly focuses on the common use of transactions:
- Some "pits" in which transactions do not take effect, and how to avoid or solve them.
- The effect of different transaction propagation mechanisms in actual code.
- The problem of large affairs and the related processing methods.
(2) Some pits where transactions do not take effect
First, let's take a look at some pits where transactions do not take effect. Similarly, we need to build a table as a test at the beginning:
CREATE TABLE `t_trans_test` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `amount` decimal(16,0) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
Here, two pieces of data are constructed to represent the balance of two people respectively:
INSERT INTO `transaction_tb`.`t_trans_test` (`id`, `name`, `amount`) VALUES (1, 'user A', 1000); INSERT INTO `transaction_tb`.`t_trans_test` (`id`, `name`, `amount`) VALUES (2, 'user B', 500);
1. Non transaction method calls transaction method
If you call a transaction method inside a non transaction method, whether annotated or not, you will find that the transaction will not roll back when an exception is thrown, which is also the most common case. As described in the following code, suppose we use a to transfer money to B, A-100 and B+100. If you add a transaction annotation to B, but a does not add it, You will find that the operation of a is not rolled back:
Note: this operation is only for demonstration, and it is absolutely not the case for deduction and recharge at work.
// Random exception trigger flag private static final boolean EXCEPTIN_FLAG = true; @Autowired private JdbcTemplate jdbcTemplate; /** * The first case of transaction invalidation: non transactional methods call transactional methods */ // It is normal to release the following annotation // @Transactional(rollbackFor = Exception.class) public void nonTransactionUseTrasaction(){ System.out.println("Initiate transfer"); jdbcTemplate.update("update t_trans_test set amount=amount-100 where name='user A'"); innerMethod(); } @Transactional(rollbackFor = Exception.class) public void innerMethod(){ System.out.println("Accept transfer"); if(EXCEPTIN_FLAG){ throw new RuntimeException("Transfer failed"); } jdbcTemplate.update("update t_trans_test set amount=amount+100 where name='user B'"); }/*Operation results: =========== Prepare reset data before operation======== Initiate transfer Accept transfer ====== Query operation results after running====== {id=1, name=User A, amount=900} {id=2, name=User B, amount=500} */
Solution:
There are two solutions. One is to add A rollback annotation on transaction A, which does not need to be explained too much. However, if we sometimes want to lock the transaction scope in the desired method in an internal method, the most common method is "inject yourself", and the other way is to allow the acquisition of Spring proxy objects, And the operation is completed by obtaining the proxy object of Spring. In Spring 5 New acquisition methods are added in versions after 0. For example, AopTestUtils is available in Spring test. Let's summarize these methods. If readers are interested, they can actually verify them:
- Inject "yourself".
- Use aopcontext currentProxy().
- Inject ApplicationContext to get the proxy object.
// @Transactional(rollbackFor = Exception.class) public void nonTransactionUseTrasaction(){ System.out.println("Initiate transfer"); // jdbcTemplate.update("update t_trans_test set amount=amount-100 where name = 'user A'); // Common method: inject yourself // transactionTestService.innerMethod(); // Use proxy object method 1: aopcontext currentProxy() // TransactionTestService transactionTestService = (TransactionTestService) AopContext.currentProxy(); // transactionTestService.innerMethod(); // The third method is to inject ApplicationContext // TransactionTestService transactionTestService = (TransactionTestService) applicationContext.getBean("transactionTestService"); // transactionTestService.innerMethod(); // Supplement - get class: aoputils getTargetClass(yourObjectInstance); // AopUtils.getTargetClass(yourServiceClass); } @Transactional(rollbackFor = Exception.class) protected void innerMethod(){ jdbcTemplate.update("update t_trans_test set amount=amount-100 where name='user A'"); System.out.println("Accept transfer"); if(EXCEPTIN_FLAG){ throw new RuntimeException("Transfer failed"); } jdbcTemplate.update("update t_trans_test set amount=amount+100 where name='user B'"); }
There are more ways to welcome discussion.
2. Throw an exception that is not rolled back
This is also a very common situation, so it is often recommended to add a rollBackFor to the annotation. Even if the default rule is used, it is also recommended to mark it. Although the following method throws an exception, it can be found that the final data is not rolled back s:
/** * Throw an unhandled exception * @throws SQLException */ @Transactional(rollbackFor = RuntimeException.class) public void throwErrorException() throws SQLException { jdbcTemplate.update("update t_trans_test set amount=amount-100 where name='user A'"); System.out.println("Accept transfer"); if(EXCEPTIN_FLAG){ throw new SQLException("Transfer failed"); } jdbcTemplate.update("update t_trans_test set amount=amount+100 where name='user B'"); }/*Operation results: ========== Prepare reset data before operation======== No exception test that supports rollback was thrown Accept transfer ====== Query operation results after running====== {id=1, name=User A, amount=900} {id=2, name=User B, amount=500} */
3. "Abnormal swallowing"
Swallowing exceptions is also common. It is often found that you forget to throw exceptions. Of course, the actual situation is not so obvious. Most of them are not thrown after catching exceptions:
/** * No exception is thrown, resulting in no rollback */ @Transactional(rollbackFor = Exception.class) public void nonThrowException(){ System.out.println("Initiate transfer"); jdbcTemplate.update("update t_trans_test set amount=amount-1100 where name='user A'"); List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from t_trans_test where name='user A'"); Map<String, Object> stringObjectMap = maps.get(0); BigDecimal amount = new BigDecimal(stringObjectMap.get("amount").toString()); if(amount.compareTo(BigDecimal.ZERO) < 0){ log.error("The balance does not support deduction"); } }/*Operation results: =========== Prepare reset data before operation======== No exception thrown test Initiate transfer : The balance does not support deduction ====== Query operation results after running====== {id=1, name=User A, amount=-100} {id=2, name=User B, amount=500} */
4. The database engine does not support
In most cases, this kind of situation will not happen, but if it is really difficult to think of it, here we can directly change the table and try it:
alter table t_trans_test ENGINE=MyISAM;
After modifying the table, you will find that there is no way to roll back the transaction anyway.
5. Non "public" methods
It is believed that there will be few mistakes, because if you put a transaction annotation on a method that is not public, some editors, such as IDEA, will directly prompt, as follows:
Methods annotated with '@Transactional' must be overridable
It must be noted that it must be public. Some students may say that the IDEA is set to protected and the default IDEA also reports an error, but it is not rolled back. I can only say that we can't rely too much on or trust the compiler
(3) Actual combat of transaction propagation characteristics
The following is another key point of transaction, which must be mastered, that is, the actual use effect of transaction isolation level in the code. Let's try all the cases below:
characteristic | Propagation level | explain |
---|---|---|
Current transaction support: | TransactionDefinition.PROPAGATION_REQUIRED | If a transaction currently exists, join the transaction; If there is no current transaction, a new transaction is created. |
Current transaction support: | TransactionDefinition.PROPAGATION_SUPPORTS | If a transaction currently exists, join the transaction; If there are currently no transactions, continue to run in a non transactional manner. |
Current transaction support: | TransactionDefinition.PROPAGATION_MANDATORY | If a transaction currently exists, join the transaction; Throw an exception if there is no current transaction. (mandatory: mandatory) |
The current transaction is not supported: | TransactionDefinition.PROPAGATION_REQUIRES_NEW | Create a new transaction. If there is a current transaction, suspend the current transaction. |
The current transaction is not supported: | TransactionDefinition.PROPAGATION_NOT_SUPPORTED | Run in non transactional mode. If there is a transaction currently, suspend the current transaction. |
The current transaction is not supported: | TransactionDefinition.PROPAGATION_NEVER | Run in non transactional mode, and throw an exception if there is a transaction currently. |
Other situations | TransactionDefinition.PROPAGATION_NESTED | If a transaction currently exists, create a transaction to run as a nested transaction of the current transaction; If there is no transaction at present, this value is equivalent to transactiondefinition PROPAGATION_ REQUIRED. |
1. PROPAGATION_REQUIRED
This level indicates that if there is a transaction, the transaction will be joined; If there is no current transaction, a new transaction is created. This means that if there are nested transaction operations, the of the outermost transaction will prevail by default. Here you can verify:
/** * 1. TransactionDefinition.PROPAGATION_REQUIRED test * Call at different isolation levels */ //TransactionDefinition.PROPAGATION_REQUIRED @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void required(){ jdbcTemplate.update("update t_trans_test set amount=amount-100 where name='user A'"); transactionTestService.requiredInner(); if(EXCEPTIN_FLAG){ throw new RuntimeException("Rollback transaction"); } }/*Operation results: =========== Prepare reset data before operation======== ====== Query operation results after running====== {id=1, name=User A, amount=1000} {id=2, name=User B, amount=500} java.lang.RuntimeException: Rollback transaction */ // Note that this will throw an exception // @Transactional(rollbackFor = RuntimeException.class, propagation = Propagation.NEVER) public void requiredInner(){ jdbcTemplate.update("update t_trans_test set amount=amount+100 where name='user B'"); }
It can be seen from the running results that if a transaction exception is thrown in the outer layer, even if the internal method seems to be direct, the final result will still be rolled back. This is also a very common way, so it is understandable that Spring sets this level as the default level.
2. PROPAGATION_SUPPORTS
If a transaction currently exists, join the transaction; If there are currently no transactions, continue to run in a non transactional manner. It's easy to understand this way, that is, if there is me, I will use the transaction. If there is no me, what should I do? It's more casual, and the transactiondefinition PROPAGATION_ The biggest difference of required is that it will not be created actively when there is no transaction.
Here we look at a strange phenomenon. Readers can think about why the following situations occur:
/** * * Propagation.SUPPORTS test * */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.SUPPORTS) public void required(){ jdbcTemplate.update("update t_trans_test set amount=amount-100 where name='user A'"); transactionTestService.requiredInner(); if(EXCEPTIN_FLAG){ throw new RuntimeException("Rollback transaction"); } }/*Operation results: =========== Prepare reset data before operation======== ====== Query operation results after running====== {id=1, name=User A, amount=900} {id=2, name=User B, amount=600} java.lang.RuntimeException: Rollback transaction */ public void requiredInner(){ jdbcTemplate.update("update t_trans_test set amount=amount+100 where name='user B'"); }
As can be seen from the above code, after changing the propagation characteristics of transactions, we found that even if exceptions were thrown externally or internally, there was no rollback, that is, the current code did not use transactions!!! If we need to make them in the same transaction, we can make the following adjustments. After the following code is adjusted, it is found that there are transactions in the outer layer in the internal transaction, so it will be added to the outer transaction. However, if the annotation of the outer layer is removed at this time, it will still be executed in a non transactional manner.
/** * 2. Propagation.SUPPORTS test * 3. */ @Transactional(rollbackFor = Exception.class) public void required(){ jdbcTemplate.update("update t_trans_test set amount=amount-100 where name='user A'"); transactionTestService.requiredInner(); if(EXCEPTIN_FLAG){ throw new RuntimeException("Rollback transaction"); } }/*Operation results: After repair: =========== Prepare reset data before operation======== ====== Query operation results after running====== {id=1, name=User A, amount=1000} {id=2, name=User B, amount=500} java.lang.RuntimeException: Rollback transaction */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.SUPPORTS) public void requiredInner(){ jdbcTemplate.update("update t_trans_test set amount=amount+100 where name='user B'"); }
You can see that this propagation level is easy to make mistakes, so if necessary, you must throw an exception manually and test it yourself.
3. PROPAGATION_MANDATORY
This level is relatively simple and rough, which means that there must be mistakes, or I will report errors. Here, we can simply modify the communication level in the previous section to see the effect:
/** * Propagation.SUPPORTS test */ // @Transactional(rollbackFor = Exception.class) public void required(){ jdbcTemplate.update("update t_trans_test set amount=amount-100 where name='user A'"); transactionTestService.requiredInner(); if(EXCEPTIN_FLAG){ throw new RuntimeException("Rollback transaction"); } }/*Operation results: org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory' */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.MANDATORY) public void requiredInner(){ jdbcTemplate.update("update t_trans_test set amount=amount+100 where name='user B'"); }
4. PROPAGATION_NOT_SUPPORTED
Create a new transaction. If there is a current transaction, suspend the current transaction. This level setting means that the internal nested transactions are independent. If there is an internal error, a separate transaction operation will be started. However, after the actual operation, we find that an error is reported and an exception of CannotAcquireLockException is thrown. After the exception search, it is found that the data line of the database is deadlocked. Why is it deadlocked? It is suggested that readers can think for themselves first and then search the answer. Here, it is directly said that the update mechanism of mysql is involved. An update operation uses an exclusive lock. At this time, according to the current propagation level setting, the transaction will be suspended, but the suspended lock will not be released, Therefore, when the internal method is executed, it is wrong to wait for the timeout because the lock cannot be obtained all the time:
/** PROPAGATION_NOT_SUPPORTED test */ @Transactional(rollbackFor = Exception.class) public void required(){ jdbcTemplate.update("update t_trans_test set amount=amount-100 where name='user A'"); transactionTestService.requiredInner(); if(EXCEPTIN_FLAG){ throw new RuntimeException("Rollback transaction"); } }/*Operation results: ====== Query operation results after running====== {id=1, name=User A, amount=1000} {id=2, name=User B, amount=500} org.springframework.dao.CannotAcquireLockException: StatementCallback; SQL [update t_trans_test set amount=amount+100 where name='User B']; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.NOT_SUPPORTED) public void requiredInner(){ jdbcTemplate.update("update t_trans_test set amount=amount+100 where name='user B'"); if(EXCEPTIN_FLAG){ throw new RuntimeException("Rollback transaction"); } }
After adjusting the execution sequence of transactions, we can see that internal transactions are rolled back, but external transactions are not rolled back, which also means that external transactions are directly invalidated after internal transactions are rolled back:
@Transactional(rollbackFor = Exception.class) public void required(){ transactionTestService.requiredInner(); jdbcTemplate.update("update t_trans_test set amount=amount-100 where name='user A'"); if(EXCEPTIN_FLAG){ throw new RuntimeException("Rollback transaction"); } }/*Operation results: =========== Prepare reset data before operation======== ====== Query operation result after running ======= {id=1, name=User A, amount=1000} {id=2, name=User B, amount=600} java.lang.RuntimeException: Rollback transaction */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.NOT_SUPPORTED) public void requiredInner(){ jdbcTemplate.update("update t_trans_test set amount=amount+100 where name='user B'"); if(EXCEPTIN_FLAG){ throw new RuntimeException("Rollback transaction"); } }
5. PROPAGATION_NEVER
It means that there can be no transactions and errors will be reported if there are transactions. This situation will not be demonstrated. It is better to use annotations than not. At present, I have not seen the actual use scenario of this annotation.
6. PROPAGATION_NESTED
If a transaction currently exists, create a transaction to run as a nested transaction of the current transaction; If there is no transaction at present, this value is equivalent to transactiondefinition PROPAGATION_ REQUIRED.
7, Summary
There are many contents in this article. Spring transactions are now regarded as a relatively basic thing. It is not particularly difficult to find the code. If you are familiar with the design pattern, you can basically understand the design of spring. Let's summarize the general contents of this article.
Introduction to transaction: at the beginning of this article, we introduced the concept of database transaction. Transaction is the key to the popularity of database, the characteristics of transaction ACID, the problem of concurrent transaction, the four main isolation levels of transaction, and briefly introduced the use of savepoints in transaction. These are the most basic contents of transaction and the basic knowledge for us to understand Spring transaction.
Spring transactions: the design of spring transactions takes into account the transaction management of different data sources and multiple data sources. At the beginning, we focused on listing the three core components of spring transactions, abstracting a transaction operation into three operations, obtaining connections, creating commit transactions, and rolling back transactions, which are consistent with the transaction operations and characteristics mentioned above. Then, based on the transaction isolation level, spring designed the feature, that is, the transaction propagation level, to realize different transaction features. Finally, we can take a look at the design idea of the whole S print.
After introducing Spring's transactions, we begin to introduce the implementation of programmatic transactions and declarative transactions. At the same time, we briefly introduce the core of transaction design from the perspective of source code. For example, transaction isolation between threads is stored and separated through ThreadLocal, which is also the core part of using the same transaction when implementing nested transactions, After introducing the programmed transaction, the core of the transaction is the declarative transaction. The declarative transaction architecture is much more complex than the programmed transaction, requires more time to digest, and requires a certain IOC container and knowledge of automatic configuration injection.
In the final practical part, we summarized some pits where the transaction does not take effect and an actual battle of the transaction propagation feature. Basically, we listed the basic usage scenarios of Spring transaction annotation.
Write at the end
The first article in 2022, which has been written for a long time, mainly spent reading and understanding the source code. I don't know how many people can stick to it until the end. If there are any suggestions or errors in the article, you are welcome to point out.
Finally, I wish readers good health and success in the new year.
Shoulders Of Giants
- How to manage transactions in the original way: https://cloud.tencent.com/developer/article/1697221
- Spring Transaction Management: @Transactional In-Depth
- Spring source code analysis: declarative transaction sorting