“This is the fourth day of my participation in the August More Text Challenge. For more details, see August More Text Challenge
One, foreword
In Spring, database transactions are serviced using AOP techniques. With native JDBC operations, there are a lot of try{}catch{}finally{} statements, so there is a lot of redundant code, such as opening and closing database connections and rolling back database transactions. With Spring’S AOP, this redundant code is all taken care of.
Review JDBC database transactions
Let’s review some of the annoying snippets we wrote when we first started using JDBC operations.
@Service
public class JdbcTransaction {
@Autowired
private DataSource dataSource;
public int insertStudent(Student student) {
Connection connection = null;
int result = 0;
try {
// Get the data connection
connection = dataSource.getConnection();
// Start the transaction
connection.setAutoCommit(false);
// Set the isolation level
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
/ / SQL execution
PreparedStatement prepareStatement = connection.prepareStatement("insert into t_student(name,gender,age) values (? ,? ,?) ");
prepareStatement.setString(1, student.getName());
prepareStatement.setString(2, student.getGender());
prepareStatement.setInt(3, student.getAge());
result = prepareStatement.executeUpdate();
// Commit the transaction
connection.commit();
} catch (Exception e1) {
if(connection ! =null) {
try {
// Roll back the transaction
connection.rollback();
} catch (Exception e2) {
e2.printStackTrace();
}
}
e1.printStackTrace();
} finally {
try {
if(connection ! =null && !connection.isClosed()) {
// Close the connectionconnection.close(); }}catch(SQLException e) { e.printStackTrace(); }}returnresult; }}Copy the code
The following line of business code is the most concerned. In every business code that uses JDBC, you can often see database connections being fetched and closed, transactions being committed and rolled back, lots of try… catch… finally.. Statements can fill blocks of code. We just want to execute a simple piece of SQL code. If more than one SQL is executed, the code is obviously more difficult to control.
PreparedStatement prepareStatement = connection.prepareStatement(“insert into t_student(name,gender,age) values (? ,? ,?) “); prepareStatement.setString(1, student.getName()); prepareStatement.setString(2, student.getGender()); prepareStatement.setInt(3, student.getAge()); result = prepareStatement.executeUpdate();
With continuous development and optimization, using ORM frameworks like MyBatis or Hibernate can reduce some code, but still can not reduce the opening and closing of database connection and transaction control code, but we can use AOP to extract these common code, separately implemented.
3. Database transaction isolation level
3.1 Basic characteristics of database transactions
- Atomicity: All operations in a transaction are either completed or not, and do not end somewhere in between. Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback Rollback
- Consistency: The integrity of the database is not compromised before and after a transaction begins. This means that the data must be written in full compliance with all preset rules, including the accuracy of the data, its concatenation, and the ability of subsequent databases to spontaneously do the predetermined work.
- Isolation: The ability of a database to allow multiple concurrent transactions to read, write, and modify its data at the same time. Isolation prevents data inconsistency due to cross-execution when multiple transactions are concurrently executed. There are different levels of transaction isolation, including Read Uncommitted, Read COMMITTED, repeatable Read, and Serializable. That’s what this video is about.
- They look permanent: After the transaction ends, data changes are permanent and won’t be lost even if the system fails.
Here are many examples of isolation that are used on the Internet. Let’s say I have a stock of 100 items, and I can only snap up one item at a time. All sorts of things happen:
moment | Transaction 1 | Transaction 2 |
---|---|---|
T1 | Initial stock 100 | Initial stock 100 |
T2 | After the deduction of inventory, the remaining is 99 | |
T3 | After the deduction of inventory, the remaining is 99 | |
T4 | Submit transaction, merchandise inventory is 99 | |
T5 | To roll back the transaction, the item inventory is 100 |
As described above, when one transaction rolls back another transaction commit, the resulting data inconsistency is referred to as a lost update of the first kind. The first type of missing update problem, which is described in the above table, has been addressed by current databases.
moment | Transaction 1 | Transaction 2 |
---|---|---|
T1 | Initial stock 100 | Initial stock 100 |
T2 | After the deduction of inventory, the remaining is 99 | |
T3 | After the deduction of inventory, the remaining is 99 | |
T4 | Commit transaction, remaining inventory 99 | |
T5 | Commit transaction, remaining inventory 99 |
Transaction 1 is not aware of the operation of transaction 2, so transaction 1 does not know that transaction 2 modified the data, so it thinks that only one business occurred, so the inventory is still changed to 99 by transaction 1. The commit of transaction 1 at time T5 leads to the loss of the commit result of transaction 2, so the data loss update caused by multiple transactions is called the second type of loss update. This is what Internet systems focus on, and to overcome this problem, databases introduce the concept of isolation levels between transactions.
3.2 Detailed database isolation Levels
To suppress lost updates, the database standard kicks four isolation levels to suppress lost updates to varying degrees. The four isolation levels mentioned above — uncommitted reads, read/write commits, repeatable reads, and serialization — suppress missing updates to varying degrees.
3.2.1 Read is not committed
Uncommitted reads are the lowest isolation level for a database and allow one transaction to read data that is not committed by another transaction. Uncommitted read is a dangerous isolation level, which is generally not widely used in actual development. Its biggest advantage is that it has high concurrency capability and is suitable for some scenarios that have no requirements on data consistency but pursue high concurrency. Its biggest disadvantage is that it may cause dirty reads.
moment | Transaction 1 | Transaction 2 | note |
---|---|---|---|
T0 | The initial inventory of the item is 100 | ||
T1 | Read inventory 100 | ||
T2 | Deducting the inventory | Now the inventory is 99 | |
T3 | Deducting the inventory | At this point, the inventory is 98 and the uncommitted data from transaction 1 is read | |
T4 | Commit the transaction | The inventory is kept at 98 | |
T5 | Roll back the transaction | Because the first type of missing update has been resolved, the inventory will not roll back to 100, at which point the inventory is 98 |
The problems in the table above can occur if the data uses an isolation level with uncommitted reads. This phenomenon is called dirty read. Transaction 2 reads data that transaction 1 has not committed, and when transaction 1 rolls back, the data becomes dirty. Dirty reads are overcome at the isolation level of read and write commits.
3.2.2 read submitted
The read-write commit isolation level means that a transaction can only read committed data from another transaction, but not uncommitted data.
moment | Transaction 1 | Transaction 2 | note |
---|---|---|---|
T0 | The initial inventory of the item is 100 | ||
T1 | Read inventory 100 | ||
T2 | Deducting the inventory | Now the inventory is 99 | |
T3 | Deducting the inventory | At this point, the inventory is 99, and the uncommitted data of transaction 1 cannot be read | |
T4 | Commit the transaction | Inventory is kept at 99 | |
T5 | Roll back the transaction | Because the first type of missing update has been resolved, the inventory does not roll back to 100, at which point the inventory is 99 |
This is called read commit. If a transaction updates the data, the read operation must wait for the update transaction to commit before reading the data, which can solve the dirty read problem. But in this case, if transaction 2 is not committed, transaction 1 is uncommitted, the inventory of the query is 99, transaction 1 commits transaction 2 and then the inventory of the query is 98, then you have a transaction scope where two identical queries return different data, which is called non-repeatable reads.
This is the most common isolation level in various systems and is the default isolation level for SQL Server and Oracle. This isolation level can effectively prevent dirty reads unless the lock is displayed in the query, such as:
select * from T where ID=2 lock in share mode;
select * from T where ID=2 for update;
Copy the code
It is clear that the read-commit isolation level causes non-repeatable reads, while the repeatable read isolation level resolves non-repeatable reads.
3.2.3 Repeatable Read
The goal of repeatable reads is to overcome the phenomenon of non-repeatable reads in a read commit, because during a read commit, some values may change and affect the execution of the current transaction.
moment | Transaction 1 | Transaction 2 | note |
---|---|---|---|
T0 | The initial inventory of the item is 100 | ||
T1 | Read inventory 100 | ||
T2 | Deducting the inventory | Now the inventory is 99 | |
T3 | Read the inventory | No reads allowed. Transaction 1 is not committed | |
T4 | Commit the transaction | Inventory is kept at 99 | |
T5 | Read the inventory | Now the inventory is 99 |
moment | Transaction 1 | Transaction 2 | note |
---|---|---|---|
T0 | The initial inventory of the item is 100 | ||
T1 | Read inventory 100 | ||
T2 | Querying order Records | 0 order records | |
T3 | Buckle inventory | Inventory is kept at 99 | |
T4 | Insert an order record | An order record is added | |
T5 | Commit the transaction | At this time, the inventory is 99, and 1 order record | |
T6 | Querying order Records | For one order record, there is a query inconsistency, and transaction 2 appears to have a result that is inconsistent with the previous query |
3.2.4 serialization
Serialization is the highest isolation level for a database and requires that all SQL be executed sequentially, thus overcoming all of the problems described above and ensuring data consistency. But the performance is also the worst.
3.2.5 Summary of isolation levels
Isolation level | Dirty read | Unrepeatable read | To celebrate the |
---|---|---|---|
Uncommitted read | Square root | Square root | Square root |
Read the submission | x | Square root | Square root |
Repeatable read | x | x | Square root |
serialization | x | x | x |
Oracle supports only read commit and serialization, while MySQL supports all four. Oracle’s default isolation level is read commit and MySQL’s is repeatable read.
4. Database transaction propagation behavior
Propagation behavior is a matter of strategy taken to invoke transactions between methods. In general, database transactions either all succeed or all fail. In Spring, when a method calls another method, the transaction can take a different strategy, such as creating a new transaction or suspending the current transaction, which is called transaction propagation. For example, below deleteStudent() calls findStudentById(ID) to check if the Student exists.
@Service
public class StudentServiceImpl implements IStudentService {
@Autowired
private StudentRepository studentRepository;
@Transactional(rollbackFor = RuntimeException.class)
@Override
public Student findStudentById(Long id) {
returnstudentRepository.getOne(id); }...@Transactional(rollbackFor = RuntimeException.class)
@Override
public void deleteStudent(Long id) {
Student student = findStudentById(id);
if(student == null) {// data not exists} studentRepository.deleteById(id); }... }Copy the code
Transaction propagation behavior supported in Spring:
package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionDefinition;
public enum Propagation {
/** * supports the current transaction and creates a new transaction if none exists. Similar to the EJB transaction attribute of the same name. * This is the default setting for transaction annotations. * /
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
/** * Supports the current transaction and executes non-transactionally if it does not exist. Similar to the EJB transaction attribute of the same name. Note: For transaction managers with transaction synchronization, SUPPORTS is slightly different from no transaction at all, because it defines the scope of the transaction to which synchronization will be applied. * Therefore, the same resources (JDBC connections, Hibernate sessions, etc.) are shared across the specified scope. Note that this depends on the actual synchronization configuration */ of the transaction manager
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
/** * supports the current transaction and throws an exception if none exists. Similar to the EJB transaction attribute of the same name. * /
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
/** * Create a new transaction, if existing, pause the current transaction. Similar to the EJB transaction attribute of the same name. * Note: Actual transaction pauses do not work out of the box on all transaction managers. * this is especially applicable to org. Springframework. Transaction. The jta. JtaTransactionManager *. It requires javax.mail transaction. TransactionManager available on it (in the standard Server specific in Java EE) */
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
/** * execute in a non-transactional manner, suspending the current transaction if it exists. Similar to the EJB transaction attribute of the same name. * Note: Actual transaction pauses do not work out of the box on all transaction managers. . This is especially applicable to org springframework. Transaction. Jta. JtaTransactionManager *. It requires javax.mail transaction. TransactionManager available on it (in the standard Java Server specific in EE). * /
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
/** * execute in a non-transactional manner and throw an exception if a transaction exists. Similar to the EJB transaction attribute of the same name. * /
NEVER(TransactionDefinition.PROPAGATION_NEVER),
/** * Execute in a nested transaction if the current transaction exists, otherwise behave like REQUIRED. There is no similar feature in EJBs. * Note: The actual creation of nested transactions only applies to a particular transaction manager. Out of the box, * this only applies to the JDBC DataSourceTransactionManager. Some JTA providers may also support nested transactions. *@see org.springframework.jdbc.datasource.DataSourceTransactionManager
*/
NESTED(TransactionDefinition.PROPAGATION_NESTED);
private final int value;
Propagation(int value) {
this.value = value;
}
public int value(a) {
return this.value; }}Copy the code
Propagation behavior of the transaction. Default is Propagation.REQUIRED. Other transaction Propagation behaviors can be manually specified as follows: (1) Propagation.REQUIRED
If a transaction exists, join it, and if none exists, create a new transaction. (2) the Propagation. The SUPPORTS
If a transaction exists, join the transaction. If no transaction currently exists, it continues to run non-transactionally. (3) Propagation. MANDATORY
If a transaction exists, join the transaction. If no current transaction exists, an exception is thrown. (4) the Propagation. The REQUIRES_NEW
Create a new transaction and defer the current transaction if one exists. (5) Propagation. NOT_SUPPORTED
Run in a non-transactional manner, suspending the current transaction if one exists. (6) Propagation. NEVER
Runs in a non-transactional manner and throws an exception if a transaction currently exists. (7) Propagation. NESTED
If not, create a new transaction. If so, other transactions are nested within the current transaction.
Use of declarative transactions in Spring
Using transactions in Spring is as simple as adding the @transactional annotation to a method. Spring’s transaction management helps us do all the cumbersome try/catch code that JDBC does.
@Service
public class StudentServiceImpl implements IStudentService {...@Transactional
@Override
public Student insertStudent(Student student) {
returnstudentRepository.save(student); }... }Copy the code
Spring generates AOP functionality when the Spring context starts calling @Transactional modified methods or classes. Transaction management in Spring is AOP-based. In the previous section, the default Propagation behavior in Spring is Propagation.REQUIRED(if a transaction exists, join the transaction; if no transaction exists, join the transaction; if no transaction exists, join the transaction; Creates a new transaction. The Spring transaction manager has default Settings for the isolation level, timeout duration, read-only content, etc. Developers can simply use the @Transactional annotation or configure it if it does not meet these requirements.
The @Transactional annotation is configured to set database transactions. If an exception occurs during the execution of the application to the application written by the developer, the Spring database transaction will have a different policy based on whether or not an exception occurs. Whether or not an exception occurs, the Spring transaction manager releases the transaction resources so that the database connection is available. So it’s going to decrease
5.1@Transactional configuration attribute
Take a look at the source code for the @Transactiona annotation:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
// Execute the transaction manager by bean name
@AliasFor("transactionManager")
String value(a) default "";
// Same as the value attribute
@AliasFor("value")
String transactionManager(a) default "";
// Specify transaction Propagation behavior. Default is Propagation.REQUIRED
Propagation propagation(a) default Propagation.REQUIRED;
// Specifies the isolation level of the transaction. By default, the default isolation level of the underlying datastore is used. For example, MySQL is repeatable read
Isolation isolation(a) default Isolation.DEFAULT;
// Specify the timeout period, in seconds
int timeout(a) default- 1;
// Whether the transaction is read-only
boolean readOnly(a) default false;
// When the specified exception occurs, the transaction is rolled back. By default, all exceptions are rolled back
Class<? extends Throwable>[] rollbackFor() default {};
The // method rolls back the transaction when the specified exception name occurs. By default, all exceptions are rolled back
String[] rollbackForClassName() default {};
// The transaction is not rolled back when the specified exception occurs
Class<? extends Throwable>[] noRollbackFor() default {};
// The method does not roll back the transaction when the specified exception name occurs
String[] noRollbackForClassName() default {};
}
Copy the code
5.2 Spring’s transaction manager
Transactions in Spring are opened, rolled back, and committed by the transaction manager. At the top of Spring in the transaction interface is TransactionManager interface is empty, this really PlatformTransactionManager interface defines a method. When the introduction of other frameworks will there be any other transaction manager classes, such as HibernateTransactionManager and JpaTransactionManager is spring – the orm this dependence, spring is the official written offer. If incoming Redisson, there would be RedissonTransactionManager. The most commonly used is DataSourceTransactionManger, it is implemented in most of the transaction manager.
PlatformTransactionManager interface only three methods, the transaction, commit the transaction, and roll back the transaction. This is the most basic of transactions. Different transaction managers can implement their own functions on this basis. For example, after the spring-boot-starter- data-JPA dependency is introduced in Spring Boot, the JpaTransactionManager is automatically created as the transaction manager, so it is generally not necessary to create our own transaction manager unless there is a specific requirement.
public interface PlatformTransactionManager extends TransactionManager {
// Obtain the transaction m can also set data attributes
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
// Commit the transaction
void commit(TransactionStatus status) throws TransactionException;
// Roll back the transaction
void rollback(TransactionStatus status) throws TransactionException;
}
Copy the code
5.3 Configuring the transaction propagation behavior and isolation level
Now test BatchServiceImpl by calling StudentServiceImpl.
Insert Student
@Service
public class StudentServiceImpl implements IStudentService {
@Autowired
private StudentRepository studentRepository;
@Transactional(rollbackFor = RuntimeException.class, isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRES_NEW)
@Override
public Student insertStudent(Student student) {
returnstudentRepository.save(student); }}Copy the code
Bulk insert
@Service
public class BatchServiceImpl implements IBatchService {
@Autowired
private IStudentService studentService;
@Transactional(rollbackFor = RuntimeException.class, isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED)
@Override
public void batchInsertStudent(List<Student> students) {
for(Student student : students) { studentService.insertStudent(student); }}}Copy the code
The batchInsertStudent method uses Propagation behavior Propagation.REQUIRED, and the database isolation level uses REPEATABLE_READ. The insertStudent method called, whose propagation behavior is REQUIRES_NEW, opens a new transaction when it is called.
With respect to @Transactional self calling, the propagation behavior is invalid
The following code, calling methods from each other in the same class, invalidates the propagation behavior defined in @Transactional. In the process of self-invocation, which is called by the class itself rather than the proxy object, no AOP is generated, so the self-invocation is not managed by the transaction manager, and the code cannot be woven into the convention process. Spring weaves code into the AOP process, like a service calling another service, which is a proxy object call.
@Service
public class StudentServiceImpl implements IStudentService {
@Autowired
private StudentRepository studentRepository;
@Transactional(rollbackFor = RuntimeException.class, propagation = Propagation.REQUIRES_NEW)
@Override
public Student findStudentById(Long id) {
return studentRepository.getOne(id);
}
@Transactional(rollbackFor = RuntimeException.class, isolation = Isolation.REPEATABLE_READ)
@Override
public void deleteStudent(Long id) {
/ / since the call
Student db = findStudentById(id);
if (db == null) {
throw new RuntimeException("The data does not exist."); } studentRepository.deleteById(id); }}Copy the code
Resolve the problem that self-invocation is invalidation of transaction propagation behavior
By obtaining the IStudentService object in the Spring context, this object is a proxy object. In this case, the proxy object can be invoked to propagate behavior.
@Service
public class StudentServiceImpl implements IStudentService {
@Autowired
private StudentRepository studentRepository;
// Inject the Spring context object
@Autowired
private ApplicationContext context;
@Transactional(rollbackFor = RuntimeException.class, propagation = Propagation.REQUIRES_NEW)
@Override
public Student findStudentById(Long id) {
return studentRepository.getOne(id);
}
@Transactional(rollbackFor = RuntimeException.class, isolation = Isolation.REPEATABLE_READ)
@Override
public void deleteStudent(Long id) {
// Obtain the IStudentService from the context
IStudentService studentService = context.getBean(IStudentService.class);
Student db = findStudentById(id);
if (db == null) {
throw new RuntimeException("The data does not exist."); } studentRepository.deleteById(id); }}Copy the code
Six, summarized
This article starts with the transaction management of native JDBC, introduces the isolation level of the database and propagation behavior in Spring, and finally uses the declarative transactions of Spring. In fact, in normal use, Spring’s declarative transactions are very simple and concise, which is the internal of Spring to help us do a lot of things.