This article explains how transactions in Spring Boot are implemented

summary

We have been using @ Transactional in SpringBoot to do transaction management, but seldom thought about how SpringBoot can achieve transaction management. Today, we start from the source code to see how @ Transactional can achieve transaction management. Finally, we write a similar annotation to help us deepen our understanding by combining with the understanding of the source code.

Reading instructions: This article assumes that you have a Java foundation and basic understanding and use of transactions.

Knowledge of transactions

Before we start looking at the source code, let's review the relevant knowledge of the transaction.

1. Isolation level of transaction

Why do transactions need isolation levels? This is because in the case of concurrent transactions, if there is no isolation level, the following problems will occur:

**Dirty read: * * when A transaction modifies the data, but this modification has not yet been committed to the database, and B transaction accesses the data at the same time. As there is no isolation, the data acquired by B may be rolled back by A transaction, which leads to the problem of data inconsistency.

**Lost to modify: * * when transaction A accesses data 100 and changes it to 100-1 = 99, and transaction B reads data 100, and changes data 100-1 = 99, the final modification result of the two transactions is 99, but it is actually 98. The data modified by transaction A is lost.

**Unrepeatable read: * * refers to that when A transaction reads data X=100, B transaction changes data X=100 to X=200. When A transaction reads data X for the second time, X=200 is found, resulting in the inconsistency of data X read twice during the whole A transaction, which is called unrepeatable read.

**Phantom read: * * phantom read is similar to nonrepeatable read. Phantom reading shows that when A transaction reads the table data, there are only three pieces of data. At this time, B transaction inserts two pieces of data. When A transaction reads again, it finds that there are five records, and there are two more records for no reason, just like the illusion.

Nonrepeatable reading VS unreal reading

The key point of non repeatable reading is to modify: under the same conditions, the data you have read will be read again and the value will be different. The key point is the update operation.

The key point of unreal reading is to add or delete: under the same conditions, the number of records read out for the first time and the second time is different, and the key point is to add or delete.

Therefore, to avoid the above problems, there is the concept of isolation level in transactions. Five constants representing isolation level are defined in Spring:

2. The propagation mechanism of transactions in Spring

Why is there a set of transaction propagation mechanism in Spring? This is a transaction enhancement tool provided by Spring, which is mainly used to solve the problems of calling between methods and how to handle transactions. For example, there are methods A, method B, and method C, which call method B and method C in A.

The pseudo code is as follows:

MethodA{
    MethodB´╝Ť
    MethodC;
}
MethodB{

}
MethodC{

}

Suppose that each of the three methods has its own transaction opened, what is the relationship between them? Does MethodA rollback affect MethodB and MethodC? The transaction propagation mechanism in Spring solves this problem.

Seven transaction propagation behaviors are defined in Spring:

How to implement abnormal rollback

After reviewing the related knowledge of transactions, we will formally study how to manage transactions through @ Transactional in Spring Boot. We will focus on how to implement rollback.

In Spring, two classes, TransactionInterceptor and PlatformTransactionManager, are the core of the whole transaction module. TransactionInterceptor is responsible for intercepting the execution of methods and judging whether a transaction needs to be submitted or rolled back.

PlatformTransactionManager is the transaction management interface in Spring, which really defines how transactions are rolled back and committed. We focus on the source code of these two classes.

There are a lot of codes in the TransactionInterceptor class. I'll simplify the logic to explain:

    //Part of the following code is omitted
    public Object invoke(MethodInvocation invocation) throws Throwable {
    //Get the target method of the transaction call
        Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    //Execute call with transaction
        return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
    }

The simple logic of invokeWithinTransaction is as follows:

    //TransactionAspectSupport.class
    //Some codes are omitted
    protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
            final InvocationCallback invocation) throws Throwable {
            Object retVal;
            try {
            //Call the real method body
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                // If exception occurs, execute transaction exception handling
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
            //Finally, clean up, mainly cache and state
                cleanupTransactionInfo(txInfo);
            }
            //If there is no exception, commit the transaction directly.
            commitTransactionAfterReturning(txInfo);
            return retVal;

    }

The logic of transaction abnormal rollback completeTransactionAfterThrowing is as follows:

//Omit some codes
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
                //To determine whether a rollback is required, the logic of judgment is to see whether the transaction attribute is declared, and to determine whether the rollback is performed in the current exception.
            if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
                //Performing a rollback
                    txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
            } 
            else {
                        //Otherwise, you don't need to roll back, just commit directly.
                    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());

            }
        }
    }

The above code has explained the basic principle of Spring transaction clearly, how to judge the execution of transaction and how to roll back.

The following is the subclass of the PlatformTransactionManager interface in the code that actually performs the rollback logic. Let's take the JDBC transaction as an example. DataSourceTransactionManager is the JDBC transaction management class. Trace the above code rollback( txInfo.getTransactionStatus ()) it can be found that the final execution code is as follows:

@Override
    protected void doRollback(DefaultTransactionStatus status) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();
        if (status.isDebug()) {
            logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
        }
        try {
        //Call rollback of jdbc to rollback the transaction.
            con.rollback();
        }
        catch (SQLException ex) {
            throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
        }
    }

Summary

Here is a summary of the implementation ideas of transactions in spring. Spring mainly relies on the TransactionInterceptor to intercept the execution method body, judge whether to open the transaction, and then execute the transaction method body, which catch es exceptions, Then determine whether a rollback is needed. If it is necessary, a real transaction manager, such as DataSourceTransactionManager in JDBC, is entrusted to execute the rollback logic. The same goes for committing transactions.

Here is a flow chart to show the following ideas:

Write a note to realize transaction rollback

We have figured out the transaction execution process of Spring, so we can write an annotation to implement the function of rolling back when encountering the specified exception. Here, the persistence layer takes the simplest JDBC as an example.

First, let's sort out the requirements. First, let's note that we can implement it based on Spring's AOP. Then since it's JDBC, we need a class to help us manage the connection to determine whether the exception is rolled back or submitted. Let's do it after combing.

1. Join dependency first

             <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2. Add a comment

/**
 * @description:
 * @author: luozhou 
 * @create: 2020-03-29 17:05
 **/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyTransaction {
    //Specify exception rollback
    Class<? extends Throwable>[] rollbackFor() default {};
}

3. New connection manager

This class helps us manage the connection. The core function of this class is to bind the extracted connection object to the thread, so that it can be easily taken out in AOP processing, submitted or rolled back.

/**
 * @description:
 * @author: luozhou 
 * @create: 2020-03-29 21:14
 **/
@Component
public class DataSourceConnectHolder {
    @Autowired
    DataSource dataSource;
    /**
     * Thread binding object
     */
    ThreadLocal<Connection> resources = new NamedThreadLocal<>("Transactional resources");

    public Connection getConnection() {
        Connection con = resources.get();
        if (con != null) {
            return con;
        }
        try {
            con = dataSource.getConnection();
            //To reflect transactions, all are set to manually commit transactions
            con.setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        resources.set(con);
        return con;
    }

    public void cleanHolder() {
        Connection con = resources.get();
        if (con != null) {
            try {
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        resources.remove();
    }
}

4. Add a facet

This part is the core of transaction processing. First, get the exception class on the annotation, then catch the executed exception, judge whether the exception is the exception or its subclass on the annotation. If it is, roll back, or commit.

/**
 * @description:
 * @author: luozhou 
 * @create: 2020-03-29 17:08
 **/
@Aspect
@Component
public class MyTransactionAopHandler {
    @Autowired
    DataSourceConnectHolder connectHolder;
    Class<? extends Throwable>[] es;

    //Method to block all MyTransaction annotations
    @org.aspectj.lang.annotation.Pointcut("@annotation(luozhou.top.annotion.MyTransaction)")
    public void Transaction() {

    }

    @Around("Transaction()")
    public Object TransactionProceed(ProceedingJoinPoint proceed) throws Throwable {
        Object result = null;
        Signature signature = proceed.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method == null) {
            return result;
        }
        MyTransaction transaction = method.getAnnotation(MyTransaction.class);
        if (transaction != null) {
            es = transaction.rollbackFor();
        }
        try {
            result = proceed.proceed();
        } catch (Throwable throwable) {
            //exception handling
            completeTransactionAfterThrowing(throwable);
            throw throwable;
        }
        //Direct submission
        doCommit();
        return result;
    }
        /**
        * Perform a rollback, and finally close the connection and clean up the thread binding
        */
    private void doRollBack() {
        try {
            connectHolder.getConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            connectHolder.cleanHolder();
        }

    }
        /**
        *Execute commit, close connection and clean up thread binding
        */
    private void doCommit() {
        try {
            connectHolder.getConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            connectHolder.cleanHolder();
        }
    }
        /**
        *Exception handling: if the caught exception is the target exception or its subclass, it will be rolled back, otherwise the transaction will be committed.
        */
    private void completeTransactionAfterThrowing(Throwable throwable) {
        if (es != null && es.length > 0) {
            for (Class<? extends Throwable> e : es) {
                if (e.isAssignableFrom(throwable.getClass())) {
                    doRollBack();
                }
            }
        }
        doCommit();
    }
}

5. Test verification

Create a TB_ The structure of the test table is as follows:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_test
-- ----------------------------
DROP TABLE IF EXISTS `tb_test`;
CREATE TABLE `tb_test` (
  `id` int(11) NOT NULL,
  `email` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

SET FOREIGN_KEY_CHECKS = 1;

1) Write a Service

The saveTest method calls two insert statements and declares the @ MyTransaction transaction annotation. When it encounters NullPointerException, it rolls back. Finally, we divide by 0 and throw an ArithmeticException. Let's use unit tests to see if the data will roll back.

/**
 * @description:
 * @author: luozhou kinglaw1204@gmail.com
 * @create: 2020-03-29 22:05
 **/
@Service
public class MyTransactionTest implements TestService {
    @Autowired
    DataSourceConnectHolder holder;
        //Two sql inserts in one transaction
   @MyTransaction(rollbackFor = NullPointerException.class)
    @Override
    public void saveTest(int id) {
        saveWitharamters(id, "luozhou@gmail.com");
        saveWitharamters(id + 10, "luozhou@gmail.com");
        int aa = id / 0;
    }
        //Execute sql
   private void saveWitharamters(int id, String email) {
        String sql = "insert into tb_test values(?,?)";
        Connection connection = holder.getConnection();
        PreparedStatement stmt = null;
        try {
            stmt = connection.prepareStatement(sql);
            stmt.setInt(1, id);
            stmt.setString(2, email);
            stmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

}

2) Unit test

@SpringBootTest
@RunWith(SpringRunner.class)
class SpringTransactionApplicationTests {
    @Autowired
    private TestService service;

    @Test
    void contextLoads() throws SQLException {
        service.saveTest(1);
    }

}

The above code declares that the transaction rolls back the NullPointerException exception. In the process of running, it encounters the ArithmeticException exception, so it will not be rolled back. We refresh the database on the right and find that the data is inserted successfully, indicating that there is no rollback.

We change the rollback exception class to ArithmeticException, clear the original data and execute it again, and an ArithmeticException exception appears. At this time, we can see that the database has not recorded and added successfully, which means that things have been rolled back, indicating that our annotation has played a role.

summary

In order to solve these problems, the isolation level of transaction is introduced in the database, which includes read uncommitted, read committed, repeatable read and serialization.

The concept of transaction is enhanced in Spring. In order to solve the transaction relationship among method A, method B and method C, the concept of transaction propagation mechanism is introduced.

The transaction implementation of @ Transactional annotation in Spring is mainly implemented by the TransactionInterceptor interceptor, which intercepts the target method, and then judges whether the exception is the target exception. If it is the target exception, roll back it, or commit the transaction.

Finally, we wrote a @ MyTransactional annotation by ourselves through JDBC and Spring's AOP to realize the function of rollback when encountering the specified exception.

Author: Carpenter

Original link: https://juejin.im/post/5e7ef0bae51d4546f16bb3fb

Source network, only for learning, if there is infringement, please contact delete.

I have compiled the interview questions and answers into PDF documents, as well as a set of learning materials, including Java virtual machine, spring framework, java thread, data structure, design pattern, etc., but not limited to this.

Focus on the official account [java circle] for information, as well as daily delivery of quality articles.

file

Tags: Programming Spring JDBC Java Database

Posted on Fri, 19 Jun 2020 03:37:39 -0400 by jwaqa1