“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.