Preface

Everyone should always write about affairs. When I was writing affairs, I encountered a bit of a pit, but it did not take effect. Later, I checked it and reviewed the scenes of various transaction failures. I thought it was better to come up with a summary, so that I can check the problem next time. You can feel confident. So let's review the related knowledge of affairs first. A transaction refers to the smallest unit of operation. As a single and indivisible unit operation, either all succeed or all fail. The transaction has four characteristics ( ACID ):

  • Atomicity ( Atomicity ): The operations contained in the transaction either all succeed or all fail and roll back. There will be no intermediate state where half of the success and half of the failure are successful. For example A and B a start has 500 yuan, A to B transfer 100 , then A less money 100 , B money must be more 100 , not A less money, B did not receive the money, then the money It's gone, it doesn't conform to the atomicity anymore.
  • Consistency ( Consistency ): Consistency means that a transaction before and after the execution, keeping the overall state of the same, such as A and B a start has 500 yuan, add up to 1000 yuan, this is before the state, A to B transfer 100 , Then the final A is 400 , B is 600 , the sum of the two is still 1000 , this overall state needs to be guaranteed.
  • Isolation ( Isolation ): The first two features are for the same transaction, and isolation refers to different transactions. When multiple transactions are operating on the same data at the same time, the impact of different transactions needs to be isolated. The transactions executed concurrently cannot interfere with each other.
  • Persistence ( Durability ): Refers to if the transaction is submitted once, then the modification to the database is permanent. Even if the database fails, the modification that has occurred must exist.

Several characteristics of transactions are not exclusive to database transactions. A transaction in a broad sense is a working mechanism and the basic unit of concurrency control. It guarantees the results of operations and includes distributed transactions. But generally we talk about transactions. If not specifically mentioned, it is related to the database, because the transactions we usually talk about are basically done based on the database.

Transactions are not only applicable to databases. We can extend this concept to other components, like queue services or external system state. Therefore, "a series of data manipulation statements must be completely completed or completely failed, leaving the system in a consistent state"

test environment

We have already deployed some demo projects and used docker to quickly build the environment. This article is also based on the previous environment:

  • JDK 1.8
  • Maven 3.6
  • Docker
  • Mysql

Sample of normal rollback of transaction

A normal transaction example contains two interfaces. One is to obtain data from all users, and the other is to update user data of 061ca68ac38902, which is actually the age of each user +1 . After we operate the first one at a time, Throw an exception and see the final result:

@Service("userService")
public class UserServiceImpl implements UserService {

    @Resource
    UserMapper userMapper;

    @Autowired
    RedisUtil redisUtil;

    @Override
    public List<User> getAllUsers() {
        List<User> users = userMapper.getAllUsers();
        return users;
    }

    @Override
    @Transactional
    public void updateUserAge() {
        userMapper.updateUserAge(1);
        int i= 1/0;
        userMapper.updateUserAge(2);
    }
}

Database operation:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.aphysia.springdocker.mapper.UserMapper">
    <select id="getAllUsers" resultType="com.aphysia.springdocker.model.User">
        SELECT * FROM user
    </select>

    <update id="updateUserAge" parameterType="java.lang.Integer">
        update user set age=age+1 where id =#{id}
    </update>
</mapper>

First get all the users of http://localhost:8081/getUserList

image-20211124233731699

When calling the update interface, the page throws an error:

image-20211124233938596

An exception also appeared on the console, which means that when divided by 0, the exception:

java.lang.ArithmeticException: / by zero
    at com.aphysia.springdocker.service.impl.UserServiceImpl.updateUserAge(UserServiceImpl.java:35) ~[classes/:na]
    at com.aphysia.springdocker.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$c8cc4526.invoke(<generated>) ~[classes/:na]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783) ~[spring-aop-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.12.jar:5.3.12]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.12.jar:5.3.12]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.12.jar:5.3.12]
    at com.aphysia.springdocker.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$25070cf0.updateUserAge(<generated>) ~[classes/:na]

Then we request http://localhost:8081/getUserList again, and see that both of the data are 11 indicating that the data has not changed. After the first operation, it is abnormal and the rollback is successful:

[{"id":1,"name":"李四","age":11},{"id":2,"name":"王五","age":11}]

Then when does the transaction roll back abnormally? Let me tell you carefully:

experiment

1. Wrong engine settings

We know that Mysql actually has the concept of a database engine. We can use show engines to view the data engines supported by Mysql

image-20211124234913121

You can see the Transactions , which is transaction support, only InnoDB , that is, only InnoDB supports transactions, so if the engine is set to other transactions, it will be invalid.

We can use show variables like 'default_storage_engine' see the default database engine, we can see that the default is InnoDB :

mysql> show variables like 'default_storage_engine';
+------------------------+--------+
| Variable_name          | Value  |
+------------------------+--------+
| default_storage_engine | InnoDB |
+------------------------+--------+

Then let's see if the data table we demonstrated is also using InnoDB , we can see that it is indeed using InnoDB

image-20211124235353205

What if we modify the engine of this table to MyISAM ? Try it, here we only modify the data engine of the data table:

mysql> ALTER TABLE user ENGINE=MyISAM;
Query OK, 2 rows affected (0.06 sec)
Records: 2  Duplicates: 0  Warnings: 0

Then update , not surprisingly, still report an error, it seems that the error is no different:

image-20211125000554928

But when all the data is obtained, the first data is successfully updated, and the second data is not updated successfully, indicating that the transaction has not taken effect.

[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]

Conclusion: The engine must be set to InnoDB for the transaction to take effect.

2. The method cannot be private

The transaction must be the public method. If it is used in the private method, the transaction will automatically fail, but in IDEA , as long as we write it, an error will be reported: Methods annotated with '@Transactional' must be overrideable , which means that the method added by the transaction comment must be rewriteable private method 061ca68ac38b3e cannot be rewritten, so an error was reported.

image-20211125083648166

The same final modification method, if annotated, will also report an error, because using final just does not want to be rewritten:

image-20211126084347611

Spring mainly uses radiation to obtain Bean AOP based on dynamic proxy technology to encapsulate the entire transaction. In theory, I want to call the private method and there is no problem. Using method.setAccessible(true); at the method level is fine, but maybe the Spring team I feel that the private method is an interface that developers are unwilling to disclose, and there is no need to destroy the encapsulation, which can easily lead to confusion.

Protected method possible? Can not!

Next, we will modify the code structure in order to achieve this, because the interface cannot use Portected . If the interface is used, it is impossible to use the protected method. It will report an error directly, and it must be used in the same package. We put controller and service Under the package:

image-20211125090358299

After testing, it was found that the transaction did not take effect. was still updated, and the other was not updated:

[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]

Conclusion: It must be used in the public method, and cannot be used in the private , final , static method, otherwise it will not take effect.

3. The exception must be a runtime exception

Springboot manages exceptions, only runtime exceptions ( RuntimeException and its subclasses) will be rolled back. For example, the i=1/0; we wrote earlier will generate runtime exceptions.

From the source code, you can also see that the rollbackOn(ex) method will determine whether the exception is RuntimeException or Error :

    public boolean rollbackOn(Throwable ex) {
        return (ex instanceof RuntimeException || ex instanceof Error);
    }

Exceptions are mainly divided into the following types:

All exceptions are Throwable , and Error is an error message, usually some uncontrollable error occurred in the program, such as no such file, memory overflow, IO suddenly wrong. Exception , except for RuntimeException , the others are all CheckException , which is an exception that can be handled. The Java program must be handled when writing, otherwise the compilation will not pass.

From the figure below, we can see that CheckedException , I have listed several common IOException IO exceptions, NoSuchMethodException did not find this method, ClassNotFoundException did not find this category, and RunTimeException has a few common ones:

  • Array out of bounds exception: IndexOutOfBoundsException
  • Type conversion exception: ClassCastException
  • Null pointer exception: NullPointerException

The transaction is rolled back by default: runtime exception, that is, RunTimeException , if other exceptions are thrown, it cannot be rolled back, such as the following code, the transaction will fail:

    @Transactional
     public void updateUserAge() throws Exception{
        userMapper.updateUserAge(1);
        try{
            int i = 1/0;
        }catch (Exception ex){
            throw new IOException("IO异常");
        }
        userMapper.updateUserAge(2);
    }

4. Caused by incorrect configuration

  1. The method needs to use @Transactional to start the transaction
  2. When configuring multiple data sources or multiple transaction managers, note that if you operate the database A , you cannot use B . Although this problem is naive, sometimes it is difficult to find the problem with the wrong one.
  3. If Spring , you need to configure @EnableTransactionManagement to open the transaction, equivalent to the configuration xml file *<tx:annotation-driven/>* , but in Springboot are no longer needed, in springboot in SpringBootApplication contains notes @EnableAutoConfiguration notes, automatically injected.

@EnableAutoConfiguration automatically injected into 061ca68ac38fa4? There is an automatic injection configuration under jetbrains://idea/navigate/reference?project=springDocker&path=~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.5.6/spring-boot-autoconfigure-2.5.6.jar!/META-INF/spring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
...
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
...

TransactionAutoConfiguration is configured inside, which is the transaction automatic configuration class:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {
  ...
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnBean(TransactionManager.class)
    @ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
    public static class EnableTransactionManagementConfiguration {

        @Configuration(proxyBeanMethods = false)
        @EnableTransactionManagement(proxyTargetClass = false)   // 这里开启了事务
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
        public static class JdkDynamicAutoProxyConfiguration {

        }
    ...

    }

}

It is worth noting that in @Transactional can also be used for classes, which means that all public methods of this class will configure transactions.

5. Transaction methods cannot be called in the same class

The methods that want to perform transaction management can only be called in other classes, and cannot be called in the current class, otherwise it will be invalid. In order to achieve this purpose, if there are many transaction methods in the same class, there are other methods at this time. It is necessary to extract a transaction class, so that the layering will be clearer, avoiding the successor to call the transaction method in the same class when writing, causing confusion.

Examples of transaction failure:

For example, we change the service transaction method to:

    public void testTransaction(){
        updateUserAge();
    }

    @Transactional
     public void updateUserAge(){
        userMapper.updateUserAge(1);
        int i = 1/0;
        userMapper.updateUserAge(2);
    }

In controller , the method without transaction annotation is called, and then the transaction method is called indirectly:

    @RequestMapping("/update")
    @ResponseBody
    public int update() throws Exception{
        userService.testTransaction();
        return 1;
    }

After the call, the transaction is found to be invalid, one is updated and the other is not updated:

[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]

like this?

Spring with the face-to-face method, which only intercepts external call methods, and does not intercept internal methods.

Look Source: In fact, we call transactional methods of time, we will enter DynamicAdvisedInterceptor of public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)() method:

image-20211128125711187

AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice() is called inside, here is to get the calling chain. And the method userService.testTransaction() without the @Transactional annotation can not get the proxy call chain at all, and the method of the original class is called.

spring , if you want to proxy a method, you use aop . An identifier is definitely needed to identify which method or class needs to be proxied. spring is defined in @Transactional as a cut point. We define this flag and it will be proxied.

acting?

Spring manages our bean . The timing of the proxy is naturally bean . See which class has this logo to generate proxy objects.

SpringTransactionAnnotationParser class has a method to determine the annotations of TransactionAttribute

    @Override
    @Nullable
    public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {
        AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(
                element, Transactional.class, false, false);
        if (attributes != null) {
            return parseTransactionAnnotation(attributes);
        }
        else {
            return null;
        }
  }

6. Transaction failure under multithreading

Suppose we use transactions in the following ways in multithreading, then the transaction cannot be rolled back normally:

    @Transactional
    public void updateUserAge() {
        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        userMapper.updateUserAge(1);
                    }
                }
        ).start();
        int i = 1 / 0;
        userMapper.updateUserAge(2);
    }

Because different threads use different SqlSession , which is equivalent to another connection, the same transaction will not be used at all:

2021-11-28 14:06:59.852 DEBUG 52764 --- [       Thread-2] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2021-11-28 14:06:59.930 DEBUG 52764 --- [       Thread-2] c.a.s.mapper.UserMapper.updateUserAge    : <==    Updates: 1
2021-11-28 14:06:59.931 DEBUG 52764 --- [       Thread-2] org.mybatis.spring.SqlSessionUtils       : Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2e956409]

7. Pay attention to the reasonable use of transaction nesting

First, the transaction has a propagation mechanism:

  • REQUIRED (default): Support the use of the current transaction. If the current transaction does not exist, create a new transaction. If there is, use the current transaction directly.
  • SUPPORTS : Support the use of the current transaction. If the current transaction does not exist, the transaction will not be used.
  • MANDATORY : Support the use of the current transaction. If the current transaction does not exist, it will throw Exception , which means that it must be currently in the transaction.
  • REQUIRES_NEW : Create a new transaction, if the current transaction exists, suspend the current transaction.
  • NOT_SUPPORTED : No transaction is executed. If the current transaction exists, the current transaction is suspended.
  • NEVER : No transaction is executed, if there is a transaction currently, Exception will be thrown.
  • NESTED : Nested transaction, if the current transaction exists, it will be executed in the nested transaction. If the current transaction does not exist, it will behave like `REQUIRED

Not much to check.

The default is REQUIRED , which means that another transaction is called in the transaction. In fact, the transaction will not be recreated, but the current transaction will be reused. Then if we write nested transactions like this:

@Service("userService")
public class UserServiceImpl {
    @Autowired
    UserServiceImpl2 userServiceImpl2;
  
    @Resource
    UserMapper userMapper;
  
      @Transactional
    public void updateUserAge() {
        try {
            userMapper.updateUserAge(1);
            userServiceImpl2.updateUserAge();
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }
}

Another transaction called:

@Service("userService2")
public class UserServiceImpl2 {

    @Resource
    UserMapper userMapper;

    @Transactional
    public void updateUserAge() {
        userMapper.updateUserAge(2);
        int i = 1 / 0;
    }
}

The following error will be thrown:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

But the actual transaction was rolled back normally, and the result was correct. The reason for this problem is that the method throws an exception inside. The same transaction is used, indicating that the transaction must be rolled back, but the external The layer was catch , which was originally the same transaction, one said rollback, and the other catch stayed to prevent spring perceiving Exception . Isn’t that a contradiction? So spring reported an error saying: This transaction was identified and must be rolled back, eventually rolled back .

How to deal with it?

    1. The outer layer actively throws an error, throw new RuntimeException()
    1. Use TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); actively identify rollback
    @Transactional
    public void updateUserAge() {
        try {
            userMapper.updateUserAge(1);
            userServiceImpl2.updateUserAge();
        }catch (Exception ex){
            ex.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }

8. Relying on external networks to request rollback needs to be considered

Sometimes, we not only operate our own database, but also need to consider external requests at the same time, such as synchronizing data. If the synchronization fails, we need to roll back our own state. In this scenario, we must consider whether the network request will go wrong and how to deal with the error. , Which is the error code to succeed.

If the network timed out, it actually succeeded, but we judged it as unsuccessful and rolled back, which may cause data inconsistency. This requires the callee to support retrying. When retrying, it needs to support idempotence and keep the state consistent with multiple calls. Although the whole main process is very simple, there are still more details.

image-20211128153822791

Summarize

The transaction is Spring . Many things may have deep source code. When we use it, we should pay attention to the simulation test to see if the call can be rolled back normally. It cannot be taken for granted. People will make mistakes. In many cases, black box testing simply tests this If the abnormal data is not rolled back normally, it needs to be processed manually later. Considering the synchronization problem between systems, it will cause a lot of unnecessary trouble. The process of manually changing the database must be followed.

image-20211128154248397

[Profile of the author] :
Qin Huai, [161ca68ac3954c Qinhuai Grocery Store ], the road to technology is not at a time, the mountains are high and the rivers are long, even if it is slow, it will never stop. Personal writing direction: Java source code analysis, JDBC , Mybatis , Spring , redis , distributed, sword refers to Offer, LeetCode etc., write every article carefully, I don’t like the series of titles, I don’t like the title party, etc. , I cannot guarantee that what I have written is completely correct, but I guarantee that what I have written has been practiced or searched for information. I hope to correct any omissions or errors.

refers to all offer solutions PDF

What did I write in 2020?

Open source programming notes


秦怀杂货店
147 声望38 粉丝

山高水长,纵使缓慢,驰而不息。