Damn, why doesn't my business take effect?

foreword

Everyone should usually write about affairs. When writing affairs before, I encountered a little pit, but it did not take effect. Later, I checked and reviewed the scenarios of various affairs failure. You can have no fear. So let's review the knowledge about transactions first. A transaction refers to the smallest unit of work of an operation. As a single and indivisible unit operation, either all succeed or all fail. Transactions have four characteristics ( ACID):

  • Atomicity ( Atomicity): The operations included in the transaction are either all successful or all failed and rolled back, and there will be no intermediate state of half success and half failure. For example A, if you have Yuan at the Bbeginning , if you transfer money , then if there is less money , you must have more money. You can’t lose money, and you don’t receive money, then the money will disappear, and it will not conform to atomicity.500AB100A100B100AB
  • Consistency ( Consistency): Consistency refers to maintaining the consistency of the overall state before and after the transaction is executed. For example , there is yuan at the beginning , Aand the sum is yuan. This is the previous state. For the transfer , then the last is , yes , two Even if they add up , this overall state needs to be guaranteed.B5001000AB100A400B6001000
  • Isolation ( Isolation): The first two features are for the same transaction, while isolation refers to different transactions. When multiple transactions operate on the same data at the same time, it is necessary to isolate the impact between different transactions, and concurrency Executed transactions must not interfere with each other.
  • Persistence ( Durability): Once the transaction is committed, 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. In a broad sense, a transaction is a working mechanism and the basic unit of concurrency control. It guarantees the results of operations, and also includes distributed transactions and the like, but generally we talk about transactions. If you don't specifically refer to it, it is related to the database, because the transactions we usually talk about are basically completed based on the database.

Transactions aren't just for databases. We can extend this concept to other components, like queue services or external system state. Thus, "a sequence of data manipulation statements must either complete or fail completely to leave 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

Example of normal transaction rollback

A normal transaction example contains two interfaces, one is to get the data of all users, and the other is to update the updateuser data, which is actually the age of each user +1. Exception, look at 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 operations:

<?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 http://localhost:8081/getUserListall users and see:

image-20211124233731699

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

image-20211124233938596

The console also throws an exception, meaning division by 0, 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 again http://localhost:8081/getUserListand see that both data 11indicate that the data has not changed. After the first operation is completed, an exception occurs and the rollback is successful:

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

When is the transaction abnormally rolled back? And listen to me in detail:

experiment

1. Incorrect engine settings

We know that Mysqlthere is actually a concept of a database engine that we can show enginesuse to view Mysqlthe supported data engines:

image-20211124234913121

You can see Transactionsthat column, that is, transaction support, only InnoDB, that is, only InnoDBsupports transactions, so if the engine is set to other transactions, it will be invalid.

We can use show variables like 'default_storage_engine'the default database engine to 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 used InnoDB, and we can see that it is indeed usedInnoDB

image-20211124235353205

So what if we change the engine of the table to something MyISAMlike this? Try, 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 again update, unsurprisingly, it still throws an error, which doesn't seem to be any different:

image-20211125000554928

However, when all the data is obtained, the first data update is successful, and the second data is not updated successfully, indicating that the transaction does not take effect.

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

Conclusion: It must be set to the InnoDBengine for the transaction to take effect.

2. Methods cannot be private

A transaction must be a publicmethod. If it is used in a privatemethod, 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 annotation of the transaction must be rewritten. The privatemethod is It cannot be rewritten, so an error is reported.

image-20211125083648166

The same finalmodified method, if annotated, will also report an error, because finalit does not want to be rewritten:

image-20211126084347611

SpringIt mainly uses Beanthe annotation information obtained by radiation, and then uses the dynamic proxy technology AOPto encapsulate the entire transaction. In theory, I think privatethere is no problem in calling the method. method.setAccessible(true);It can be used at the method level, but the Springteam may feel that privatethe method is the developer's will. There is no need to destroy the encapsulation on the interface that is unwilling to expose, which can easily lead to confusion.

ProtectedIs the method possible? Can't!

Next, in order to achieve, we will magically change the code structure, because the interface cannot be used Portected. If the interface is used, it is impossible to use the protectedmethod, and an error will be reported directly, and it must be used in the same package. We put the controllersum in servicethe same package:

image-20211125090358299

After the test, it was found that the transaction did not take effect , and the result was still one update, and the other one was not updated:

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

Conclusion: It must be used in publicmethods, not in private, final, staticmethods, otherwise it will not take effect.

3. The exception must be a runtime exception

SpringbootWhen managing exceptions, only the runtime exception ( RuntimeExceptionand its subclasses) will be rolled back. For example, as we wrote earlier i=1/0;, a runtime exception will be generated.

It can also be seen from the source code that the rollbackOn(ex)method will judge whether the exception is RuntimeExceptionor 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, Errorbut error messages. Generally, some uncontrollable errors have occurred in the program, such as no such file, memory overflow, and IOsudden errors. On the other Exceptionhand, except RuntimeException, everything else CheckExceptionis an exception that can be handled. The Javaprogram must handle this exception when it is written, otherwise the compilation will not pass.

We can see from the figure below, CheckedException, I listed several common IOExceptionIO exceptions, I NoSuchMethodExceptiondid not find this method, I ClassNotFoundExceptiondid not find this class, but RunTimeExceptionthere are several common ones:

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

The default rollback of the transaction is: 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 be used @Transactionalto start the transaction
  2. When configuring multiple data sources or multiple transaction managers, pay attention to the transactions Athat cannot be used if the database is operated B. Although this problem is very naive, sometimes it is difficult to find the problem with the wrong use.
  3. If in Spring, you need to configure @EnableTransactionManagementto open the transaction, which is equivalent to the configuration xmlfile *<tx:annotation-driven/>*, but it Springbootis no longer needed in the in, in springbootthe SpringBootApplicationannotation contains the @EnableAutoConfigurationannotation, it will be injected automatically.

@EnableAutoConfigurationWhat is automatically injected? 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.factoriesThere is an auto-injected configuration under

# 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,\
...

There is one configured in it TransactionAutoConfiguration, which is the transaction auto-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 @Transactionaladdition to being used for methods, it can also be used for classes, indicating that all publicmethods of this class will configure transactions.

5. Transaction methods cannot be called in the same class

The method that wants to perform transaction management can only be called in other classes, not in the current class, otherwise it will be invalid. In order to achieve this purpose, if the same class has many transaction methods and other methods, this time there are It is necessary to extract a transaction class, so that the layering will be clearer and avoid confusion when the successor writes the transaction method in the same class.

Example of transaction failure:

For example, we change the servicetransaction method to:

    public void testTransaction(){
        updateUserAge();
    }

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

In controllerit, 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, it is found that the transaction is invalid, one is updated and the other is not updated:

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

Why is this so?

SpringThe method is wrapped with an aspect, and only the external calling method is intercepted, and the internal method is not intercepted.

Look at the source code: In fact, when we call the transaction method, the method we will DynamicAdvisedInterceptorenter public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)():

image-20211128125711187

It is called inside AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice(), and here is the call chain to get the call. For methods without @Transactionalannotations userService.testTransaction(), the proxy call chain cannot be obtained at all, and the methods of the original class are still called.

springIf you want to proxy a method aop, you must use an identifier to identify which method or class needs to be proxied. It springis defined @Transactionalas a pointcut, and if we define this identifier, it will be proxied.

When is the time to be an agent?

SpringWe have unified management bean, and the timing of the proxy is naturally beanthe process of creation. To see which class has this logo, the proxy object will be generated.

SpringTransactionAnnotationParserThis class has a method that is used to determine TransactionAttributethe annotation:

	@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 multi-threading

Suppose we use the transaction in the following way in multi-threading, 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, 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 of all, there is a propagation mechanism for transactions:

  • REQUIRED(default): support using the current transaction, if the current transaction does not exist, create a new transaction, if there is, use the current transaction directly.

  • SUPPORTS: Supports the use of the current transaction, if the current transaction does not exist, the transaction will not be used.

  • MANDATORY: Supports the use of the current transaction. If the current transaction does not exist, it will be thrown Exception, that is, it must be currently in the transaction.

  • REQUIRES_NEW: Create a new transaction, suspend the current transaction if the current transaction exists.

  • NOT_SUPPORTED: No transaction is executed, if the current transaction exists, suspend the current transaction.

  • NEVER: No transaction is executed, throws if there is currently a transaction Exception.

  • NESTED: Nested transaction, if the current transaction exists, execute in the nested transaction. If the current transaction does not exist, the behavior is the same as `REQUIRED

Not much to check.

The default is REQUIREDthat calling another transaction in a transaction will not actually recreate the transaction, but will reuse the current transaction. 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;
    }
}

will throw the following error:

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

However, the actual transaction was rolled back normally, and the result was correct. The reason for this problem is that an exception was thrown in the method inside, and the same transaction was used, indicating that the transaction must be rolled back, but the external The layer is blocked catch, and it is the same transaction. One says to roll back, and the other is blocked from being perceived . Is catchn’t that a contradiction? So the error says: This transaction is marked and must be rolled back, and it is eventually rolled back .springExceptionspring

How to deal with it?

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

8. Relying on external network requests for rollback needs to be considered

Sometimes, we not only operate our own database, but also need to consider external requests, such as data synchronization, synchronization failure, and 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 be successful.

If the network times out, it actually succeeds, but we judge it to be unsuccessful and roll it back, which may lead to data inconsistency. This requires the callee to support retry. When retrying, it needs to support idempotency, and the consistency of the saved state is called multiple times. Although the whole main process is very simple, there are still many details in it.

image-20211128153822791

Summarize

Transactions are Springwrapped in complexity, and many things may have deep source code. When we use them, we should pay attention to simulating to test whether the call can be rolled back normally. It cannot be taken for granted. People can make mistakes, and many times black-box testing simply tests this kind of exception. If the data is not rolled back normally, it needs to be processed manually later. Considering the problem of synchronization between systems, it will cause a lot of unnecessary trouble. The process of manually changing the database must be done.

image-20211128154248397

【Author's brief introduction】 :
Qin Huai, author of the public account [ Qin Huai Grocery Store ], the road of technology is not at one time, the mountains are high and the rivers are long, even if it is slow and endless. Personal writing direction: Java源码解析, JDBC, Mybatis, Spring, redis, 分布式, 剑指Offer, LeetCodeetc., write every article carefully, don't like headline parties, don't like bells and whistles, mostly write series of articles, I can't guarantee that what I write is completely correct, but I guarantee that what I write All have been practiced or searched for information. For omissions or errors, please correct me.

Sword Point Offer All Problem Solutions PDF

What did I write in 2020?

Open Source Programming Notes

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/5077784/blog/5381599