An overview of the

Has been using the @ SpringBoot Transactional for transaction management, but few have you thought about SpringBoot is how to realize the transaction management, today, from the perspective of the source code, look at how @ Transactional implementation affairs, finally, we combine the understanding of the source, To help us understand more, write a similar annotation to implement transaction management yourself.

Reading Instructions: This article assumes you have a basic Java background and a basic understanding and use of transactions.

Knowledge of transactions

Before we start looking at the source code, let’s review transactions.

1. Transaction isolation level

Why do transactions need isolation levels? This is because in the case of concurrent transactions, the absence of an isolation level can cause the following problems:

** When transaction A makes changes to data that have not yet been committed to the database and transaction B is accessing the data at the same time, the data acquired by transaction B may be rolled back by transaction A due to the lack of isolation, resulting in data inconsistency problems.

** Lost To Modify: ** Lost To Modify: ** Lost To Modify: ** Lost To Modify: ** Lost To Modify: ** Lost To Modify: ** Lost To Modify: ** Lost To Modify: ** Lost To Modify: ** Lost To Modify: ** Lost To Modify: The data modified by transaction A was lost.

** Unrepeatable Read: ** refers to that when transaction A reads data X=100, transaction B changes data X=100 to X=200. When transaction A reads data X for the second time, it finds that X=200. As A result, the two reads of data X are inconsistent during the whole transaction, which is called unrepeatable read.

Phantom Read: Phantom Read is similar to unrepeatable reading. When transaction A reads the table data, there are only 3 items of data. At this time, transaction B inserts 2 items of data. When transaction A reads the table data again, it finds 5 items of data.

Unrepeatable read vs. phantom read

The emphasis of non-repeatable reading is modification: the same condition, you read the data, read again to find a different value, the emphasis is on the update operation.

The key of magic reading is to add or delete: under the same conditions, the number of records read for the first time and the second time is not the same, the key is to add and delete operations.

So, in order to avoid the above problems, there is a concept of isolation level in transactions, and there are five constants that represent isolation level defined in Spring:

2. Transaction propagation mechanism in Spring

Why have a transaction propagation mechanism in Spring? This is a transaction enhancement tool provided by Spring, mainly to solve the problem of how to handle transactions between method calls. For example, there are methods A, B, and C, and methods B and C are called from A.

The pseudocode is as follows:

MethodA{
    MethodB;
    MethodC;
}
MethodB{

}
MethodC{

}
Copy the code

Suppose 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 addresses this problem.

Seven transaction propagation behaviors are defined in Spring:

How is exception rollback implemented

Now that you’ve reviewed transactions, let’s take a look at how Spring Boot uses @Transactional to manage transactions. Let’s focus on how it implements rollback.

In Spring and TransactionInterceptor PlatformTransactionManager these two classes is the core of the entire transaction module, TransactionInterceptor responsible for intercept method, to determine whether need to commit or rollback transaction.

PlatformTransactionManager is the transaction management interface in the Spring, how truly defines transaction rollback and submit. We focus on the source code of these two classes.

The TransactionInterceptor class has a lot of code in it, so I’ll simplify the logic.

Public Object invoke(MethodInvocation) throws Throwable {// Get the target method of the transaction invocation Class<? > targetClass = (invocation.getThis() ! = null ? AopUtils.getTargetClass(invocation.getThis()) : null); // Perform a transaction callreturn invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
    }
Copy the code

The simplified logic for invokeWithinTransaction is as follows:

/ / TransactionAspectSupport. The class / / omitted part of the code protected Object invokeWithinTransaction (Method Method, @ Nullable class <? > targetClass, final InvocationCallback invocation) throws Throwable { Object retVal; Try {/ / call the method that real body retVal = invocation. ProceedWithInvocation (); } the catch (Throwable ex) {/ / if abnormal, perform transactions exception handling completeTransactionAfterThrowing (txInfo, ex); throw ex; } finally {// cleanupTransactionInfo(txInfo) cleanupTransactionInfo(txInfo); } // If there are no exceptions, commit the transaction directly. commitTransactionAfterReturning(txInfo);return retVal;

    }
Copy the code

The abnormal rollback transaction logic completeTransactionAfterThrowing is as follows:

/ / omit part of the code protected void completeTransactionAfterThrowing (@ Nullable TransactionInfo txInfo, Throwable ex) {/ / determine whether need to roll back, The logic is to see if the transaction attributes are declared and whether a rollback is performed in the current exception.if(txInfo.transactionAttribute ! = null && txInfo. TransactionAttribute. RollbackOn (ex)) {/ / rollback txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); }else{// Otherwise, do not need to roll back, just commit. txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); }}}Copy the code

The above code has explained the basics of Spring transactions, how to determine the execution of transactions and how to roll back.

Below the real rollback logic code PlatformTransactionManager interfaces subclass, we use the JDBC transaction, for example, DataSourceTransactionManager is JDBC transaction management class. Tracing the code above the rollback (txInfo getTransactionStatus ()) can be found eventually executed 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 JDBC rollback to rollback the transaction. con.rollback(); } catch (SQLException ex) { throw new TransactionSystemException("Could not roll back JDBC transaction", ex); }}Copy the code

summary

Spring mainly relies on the TransactionInterceptor to intercept the execution method body, determine whether to start the transaction, then execute the transaction method body, catch the exception in the method body, and determine whether to roll back. If need to entrust the real rollback the TransactionManager such as JDBC DataSourceTransactionManager to rollback logic. The same goes for committing transactions.

Here’s a flow chart to illustrate the idea:

Write a note to rollback the transaction

Now that we’ve figured out Spring’s transaction execution process, we can emulate ourselves by writing an annotation that rolls back the specified exception. Here, the persistence layer takes JDBC as an example in its simplest form.

Let’s start by reviewing the requirements, first by noting that we can implement Spring based AOP, and then since it’s JDBC, we need a class to manage the connection for us and determine whether exceptions are rolled back or committed. As soon as you’re done.

1. Add dependencies 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>Copy the code

2. Add a new note

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

3. Add the connection manager

This class helps us manage connections. The core function of this class is to bind fetched connection objects to threads for easy retrieval in AOP processing for commit or rollback operations.

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

    public Connection getConnection() {
        Connection con = resources.get();
        if(con ! = null) {returncon; } try { con = dataSource.getConnection(); 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();
    }
}
Copy the code

4. Add a new section

This part is the heart of the transaction. It first gets the exception class on the annotation, then catches the exception executed, determines whether the exception is an exception on the annotation or a subclass of it, rolls back if it is, and commits otherwise.

/** * @description: * @author: luozhou * @create: 2020-03-29 17:08 **/ @Aspect @Component public class MyTransactionAopHandler { @Autowired DataSourceConnectHolder connectHolder; Class<? extends Throwable>[] es; / / intercept all MyTransaction annotation methods @ org. Aspectj. Lang. The 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(); } the catch (Throwable Throwable) {/ / exception handling completeTransactionAfterThrowing (Throwable); throw throwable; } // Commit directlydoCommit();
        returnresult; } /** * perform a rollback and finally close the connection and clean up the thread binding */ private voiddoRollBack() { try { connectHolder.getConnection().rollback(); } catch (SQLException e) { e.printStackTrace(); } finally { connectHolder.cleanHolder(); }} /** * Perform the commit and finally close the connection and clean up the thread binding */ private voiddoCommit() { try { connectHolder.getConnection().commit(); } catch (SQLException e) { e.printStackTrace(); } finally { connectHolder.cleanHolder(); If the exception is the target exception or a subclass of it, the transaction is rolled back, otherwise the transaction is 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(); }}Copy the code

5. Test and verification

Create a table tb_test with the following structure:

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;
Copy the code

1) Write a Service

The saveTest method calls two insert statements, declares the @MyTransaction transaction annotation, rolls back NullPointerException, and finally divides by 0, which throws ArithmeticException. We use unit tests to see if the data rolls back.

/** * @description: * @author: luozhou [email protected] * @create: 2020-03-29 22:05 **/ @Service public class MyTransactionTest implements TestService { @Autowired DataSourceConnectHolder  holder; / / a transaction performed in the two SQL insert @ MyTransaction (rollbackFor = NullPointerException. Class) @ Override public void saveTest (int id) { saveWitharamters(id,"[email protected]");
        saveWitharamters(id + 10, "[email protected]"); int aa = id / 0; } 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(); }}}Copy the code

2) Unit testing

@SpringBootTest @RunWith(SpringRunner.class) class SpringTransactionApplicationTests { @Autowired private TestService service; @Test void contextLoads() throws SQLException { service.saveTest(1); }}Copy the code

The above code declares the transaction to roll back the NullPointerException exception. The ArithmeticException exception is encountered, so it cannot be rolled back. We refresh the database on the right and find that the insert is successful, indicating that the rollback is not happening.

We changed the rollback exception class to ArithmeticException, we emptied the original data and ran it again, and we found ArithmeticException, and we didn’t have any new records in the database, which means that things are being rolled back, which means that our annotations are working.

conclusion

In order to solve these problems, a transaction isolation level has been introduced in the database. The isolation levels include: read uncommitted, read committed, repeatable read, and serialization.

Spring has enhanced the concept of transactions by introducing A transaction propagation mechanism to address the transaction relationship between methods A, B, and C.

The @Transactional annotation in Spring is implemented primarily through the TransactionInterceptor interceptor. It intercepts the target method, determines whether the exception is the target exception, rolls back the exception, and commits the transaction otherwise.

Finally, we wrote our own @myTransactional annotation using JDBC combined with Spring’s AOP to implement the rollback of specified exceptions.

Author: Wood carpenter

Original link: juejin.cn/post/684490…

Wenyuan network, only for the use of learning, if there is infringement please contact delete.

I’ve compiled the interview questions and answers in PDF files, as well as a set of learning materials covering, but not limited to, the Java Virtual Machine, the Spring framework, Java threads, data structures, design patterns and more.

Follow the public account “Java Circle” for information, as well as quality articles delivered daily.