1. Basic principles of transactions
The essence of Spring transactions is actually the database's support for transactions. Without database transaction support, spring cannot provide transaction functions. For pure JDBC operating databases, if you want to use transactions, you can follow the following steps:
1. Get the connection Connection con = DriverManager.getConnection()
2. Open the transaction con.setAutoCommit(true/false);
3. Execute CRUD
4. Commit transaction/rollback transaction con.commit() / con.rollback();
5. Close the connection conn.close();
After using Spring's transaction management function, we can no longer write the code in steps 2 and 4, but will be automatically done by Spirng. So how does Spring open and close transactions before and after the CRUD we write? By solving this problem, we can understand the implementation principle of Spring's transaction management from a whole. Let me briefly introduce the annotation method as an example
1. Turn on the annotation driver in the configuration file and identify it by annotating the @Transactional on the relevant classes and methods.
2. When spring starts, it will parse and generate related beans. At this time, it will check the classes and methods with relevant annotations, and generate a proxy for these classes and methods, and perform related configuration injection based on the relevant parameters of @Transaction, so that the relevant transactions are processed for us in the proxy (start normal transactions and exception rollback transactions).
3. Transaction commit and rollback of the real database layer are implemented through binlog or redo log.
2. Propagation properties of Spring transactions
The so-called propagation attribute of spring transactions defines how spring should handle the behavior of multiple transactions when they exist at the same time. These properties are defined in TransactionDefinition. The specific constants are explained in the following table:
III. Database isolation level
Dirty reading: One transaction adds, deletes and modifys the data, but is not committed, and another transaction can read uncommitted data. If the first transaction rolls back at this time, the second transaction reads dirty data.
No repetitive reading: Two read operations occur in one transaction. Between the first read operation and the second operation, the other transaction modifies the data. At this time, the data read two times are inconsistent.
Fantasy reading: The first transaction modifies data in a certain range in batches, and the second transaction adds one data to this range. At this time, the first transaction will lose the modification of the newly added data.
Summarize :
The higher the isolation level, the more it can ensure the integrity and consistency of the data, but the greater the impact on concurrency performance.
The default isolation level of most databases is Read Commissioned, such as SqlServer and Oracle
The default isolation level of a few databases is: Repeatable Read For example: MySQL InnoDB
IV. Isolation level in Spring
V. Nesting of transactions
Through the above theoretical knowledge, we roughly understand some attributes and characteristics of database transactions and spring transactions. Next, we analyze some nested transaction scenarios to deeply understand the mechanism of spring transaction propagation.
Suppose the Method A() of the outer transaction Service A calls the Method B() of the inner service B
PROPAGATION_REQUIRED(spring default)
If the transaction level of ServiceB.methodB() is defined as PROPAGATION_REQUIRED, then when the ServiceA.methodA() is executed, the transaction has already started in the spring. At this time, ServiceB.methodB() is called. ServiceB.methodB() sees that it is running inside the transaction of ServiceA.methodA(), and no new transaction is started.
If ServiceB.methodB() is running, it will assign a transaction to itself.
In this way, if an exception occurs in ServiceA.methodA() or anywhere within ServiceB.methodB(), the transaction will be rolled back.
PROPAGATION_REQUIRES_NEW
For example, we designed that ServiceA.methodA() has a transaction level of PROPAGATION_REQUIRED, and ServiceB.methodB() has a transaction level of PROPAGATION_REQUIRES_NEW.
Then when the ServiceB.methodB() is executed, the transaction where ServiceA.methodA() is located will be suspended, and ServiceB.methodB() will start a new transaction and will continue to execute after the ServiceB.methodB() transaction is completed.
The difference between his transactions and PROPAGATION_REQUIRED is the degree of rollback of the transaction. Because ServiceB.methodB() is a new transaction, then there are two different transactions. If ServiceB.methodB() has been submitted, then ServiceA.methodA() fails to rollback, ServiceB.methodB() will not rollback. If ServiceB.methodB() fails to roll back, if the exception thrown by ServiceA.methodA() is caught, the ServiceA.methodA() transaction may still be submitted (mainly depends on whether the exception thrown by B is an exception that A will rollback).
PROPAGATION_SUPPORTS
Assuming that the transaction level of ServiceB.methodB() is PROPAGATION_SUPPORTS, when it is executed to ServiceB.methodB(), if it is found that ServiceA.methodA() has opened a transaction, it will join the current transaction. If it is found that ServiceA.methodA() has not started the transaction, it will not start the transaction itself. At this time, the transactionality of the internal method depends entirely on the outermost transaction.
PROPAGATION_NESTED
The situation is becoming more complicated now. The transaction property of ServiceB.methodB() is configured as PROPAGATION_NESTED. How will the two cooperate at this time? ServiceB#methodB If rollback, then internal transactions (i.e. ServiceB#methodB) will roll back to SavePoint before it executes, while external transactions (i.e. ServiceA#methodA) can have the following two ways of handling:
a. Catch exceptions and execute exception branch logic
void methodA() { try { ServiceB.methodB(); } catch (SomeException) { // Execute other businesses, such as ServiceC.methodC(); } } This method is also the most valuable thing about nested transactions. It plays the role of branch execution. If ServiceB.methodB fails, then ServiceC.methodC() is executed, and ServiceB.methodB has rolled back to the SavePoint before it executes, so no dirty data is generated (equivalent to this method never being executed). This feature can be used in some special services, and neither PROPAGATION_REQUIRED nor PROPAGATION_REQUIRES_NEW can do this.
b. The external transaction rollback/commit code does not make any modifications. If the internal transaction (ServiceB#methodB) rollback, then first ServiceB.methodB rolls back to the SavePoint before it executes (in any case), and the external transaction (i.e. ServiceA#methodA) will decide whether it commit or rollback based on the specific configuration.
The other three transaction propagation attributes are basically ineffective, so no analysis is done here.
6. Summary
For places where transactions are needed in the project, I suggest that developers should use spring's TransactionCallback interface to implement transactions. Do not blindly use spring transaction annotations. If you must use annotations, you must have a detailed understanding of the propagation mechanism and isolation level of spring transactions, otherwise unexpected effects may occur.