- 💻 Sword finger offer & leetcode question brushing warehouse: https://github.com/Damaer/CodeSolution
- Document address: https://damaer.github.io/CodeSolution/
- Warehouse introduction: Question brushing warehouse: CodeSolution
- 📒 Programming knowledge base: https://github.com/Damaer/Coding
- Document address: https://damaer.github.io/Coding/#/
preface
We should write about affairs at ordinary times. We encountered a pit when writing affairs before, but it didn't take effect. Later, we checked and reviewed the failure scenes of various affairs. We thought we might as well make a summary, so that we can have confidence and fear in the next troubleshooting. First, let's review the relevant knowledge of transaction. Transaction refers to the smallest work unit of operation. As a separate and non cutting unit, the operation will either succeed or fail. Transactions have four characteristics (ACID):
- Atomicity: the operations contained in A transaction are either all successful or all failed. There will be no intermediate state of half success and half failure. For example, both A and B have 500 yuan at the beginning. If A transfers 100 yuan to B, then A's money is 100 less and B's money must be 100 more. If A is less and B doesn't receive the money, the money will disappear, which is not atomic.
- Consistency: consistency refers to the consistency of the overall state before and after the transaction is executed. For example, both a and B have 500 yuan at the beginning, which adds up to 1000 yuan. This is the previous state. A transfers 100 to B, and finally a is 400, B is 600, which adds up to 1000. This overall state needs to be guaranteed.
- Isolation: the first two features are aimed at the same transaction, and isolation refers to different transactions. When multiple transactions are operating the same data at the same time, it is necessary to isolate the impact between different transactions, and concurrent transactions cannot interfere with each other.
- Durability: once a transaction is committed, the changes to the database are permanent. Even if the database fails, the changes that have occurred must exist.
Several characteristics of transactions are not exclusive to database transactions. In a broad sense, transactions are a working mechanism and the basic unit of concurrency control. They ensure the results of operations and also include distributed transactions. However, generally speaking, when we talk about transactions, we don't specifically refer to them, but they are related to databases, because the transactions we usually talk about are basically completed based on databases.
Transactions are not only applicable to databases. We can extend this concept to other components, such as queue services or external system states. Therefore, "a series of data operation statements must complete or fail completely and leave the system in a consistent state"
testing environment
Previously, we have deployed some demo projects and quickly built the environment with docker. This paper is also based on the previous environment:
- JDK 1.8
- Maven 3.6
- Docker
- Mysql
Refer to:
How to quickly build a Springboot + Mysql + Redis project based on Docker
Run the projects in the IDEA in the local docker?
How to deploy a project with Docker Compose?
Example of normal transaction rollback
The normal transaction example includes two interfaces: one is to obtain the data of all users, and the other is to update the user data, which is actually the age of each user + 1. Let's throw an exception after the first operation to 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>
Get first http://localhost:8081/getUserList All users see:
image-20211124233731699
When calling the update interface, the page throws an error:
image-20211124233938596
The console also has an exception, which means divide 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 ask again http://localhost:8081/getUserList , you can see that both data are 11, which means that the data has not changed. After the first operation, the exception and rollback succeeded:
[{"id":1,"name":"Li Si","age":11},{"id":2,"name":"Wang Wu","age":11}]
When is the transaction rolled back abnormally? Listen to me carefully:
experiment
1. The engine is not set correctly
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 column, that is, transaction support. Only InnoDB supports Transactions, that is, only InnoDB supports Transactions. Therefore, if the engine is set to other Transactions, it will be invalid.
We can use show variables like 'default_storage_engine 'from the default database engine, you can see that the default is InnoDB:
mysql> show variables like 'default_storage_engine'; +------------------------+--------+ | Variable_name | Value | +------------------------+--------+ | default_storage_engine | InnoDB | +------------------------+--------+
Let's see if InnoDB is also used in the data table we demonstrate. We can see that InnoDB is indeed used
image-20211124235353205
What if we change the table engine 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, errors will still be reported. It seems that the errors are no different:
image-20211125000554928
However, when all data is obtained, the first data is successfully updated, and the second data is not successfully updated, indicating that the transaction is not effective.
[{"id":1,"name":"Li Si","age":12},{"id":2,"name":"Wang Wu","age":11}]
Conclusion: the transaction cannot take effect until it is set as an InnoDB engine.
2. The method cannot be private
The transaction must be a public method. If it is used on a private method, the transaction will automatically expire, but in the IDEA, an error will be reported as long as we write it: methods annotated with '@ transactional' must be overridable, which means that the method added to the annotation of the transaction must be rewritable, and the private method cannot be rewritten, so an error is reported.
image-20211125083648166
The same final modification method, if annotated, will also report an error, because using final does not want to be rewritten:
image-20211126084347611
Spring mainly uses radiation to obtain the annotation information of beans, and then uses AOP based on dynamic proxy technology to encapsulate the whole transaction. In theory, I think there is no problem to call private methods. Use method at the method level setAccessible(true); It's OK, but the spring team may think that the private method is an interface that developers are unwilling to disclose. There's no need to destroy the encapsulation, which can easily lead to confusion.
Is the Protected method OK? may not!
Next, in order to realize the magic change of the code structure, because the interface cannot be ported. If the interface is used, it is impossible to use the protected method, which will directly report an error, and must be used in the same package. We put the controller and service under the same package:
image-20211125090358299
After the test, it is found that the transaction does not take effect. The result is still that one is updated and the other is not updated:
[{"id":1,"name":"Li Si","age":12},{"id":2,"name":"Wang Wu","age":11}]
Conclusion: it must be used on public methods, not on private,final and static methods, otherwise it will not take effect.
3. Exceptions must be run-time exceptions
When Springboot manages exceptions, it only rolls back the runtime exceptions (RuntimeException and its subclasses), such as i=1/0;, A runtime exception will be generated.
From the source code, we can also see that the rollbackOn(ex) method will judge whether the exception is a RuntimeException or an 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, while Error is the Error message. Generally, the program has some uncontrollable errors, such as the absence of this file, memory overflow, and sudden IO Error. Under Exception, except RuntimeException, all other exceptions are CheckException, that is, exceptions that can be handled. Java programs must handle this Exception when writing, otherwise compilation will not pass.
As can be seen from the following figure, CheckedException, I listed several common ioexceptions. NoSuchMethodException did not find this method, ClassNotFoundException did not find this class, and RunTimeException has several common types:
- Array out of bounds exception: IndexOutOfBoundsException
- Type conversion exception: ClassCastException
- Null pointer exception: NullPointerException
By default, the transaction is rolled back: runtime exception, that is, RunTimeException. If other exceptions are thrown, the transaction cannot be rolled back. For example, the following code will invalidate the transaction:
@Transactional public void updateUserAge() throws Exception{ userMapper.updateUserAge(1); try{ int i = 1/0; }catch (Exception ex){ throw new IOException("IO abnormal"); } userMapper.updateUserAge(2); }
4. Caused by incorrect configuration
- You need to use @ Transactional on the method to start a transaction
- When configuring multiple data sources or multiple transaction managers, note that if database A is operated, the transactions of B cannot be used. Although this problem is childish, sometimes it is difficult to find the problem by mistake.
- If you need to configure @ EnableTransactionManagement to start a transaction in Spring, it is equivalent to configuring the xml file * < TX: annotation driven / > *, but it is no longer needed in springboot. In springboot, the SpringBootApplication annotation contains the @ EnableAutoConfiguration annotation, which will be injected automatically.
@What does EnableAutoConfiguration inject automatically? stay 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. Automatic injection is configured under 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,\ ...
There is a TransactionAutoConfiguration configured, which is the automatic transaction 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) // The transaction is opened here @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false") public static class JdkDynamicAutoProxyConfiguration { } ... } }
It is worth noting that @ Transactional can be used not only for methods, but also for classes, which means that all public methods of this class will configure transactions.
5. Transaction methods cannot be called in the same class
Methods for transaction management can only be called in other classes, not in the current class, otherwise they will become invalid. In order to achieve this purpose, if there are many transaction methods and other methods in the same class, it is necessary to extract a transaction class at this time. In this way, the hierarchy will be clearer to avoid calling transaction methods in the same class when the successor writes, Cause chaos.
Examples of transaction invalidation:
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); }
The method without transaction annotation is called in the controller, and then the transaction method is called indirectly:
@RequestMapping("/update") @ResponseBody public int update() throws Exception{ userService.testTransaction(); return 1; }
After calling, it is found that the transaction is invalid, one is updated and the other is not updated:
[{"id":1,"name":"Li Si","age":12},{"id":2,"name":"Wang Wu","age":11}]
Why?
Spring uses cut surface method to wrap, and only intercepts the external calling method, but not the internal method.
Look at the source code: in fact, when we call the transaction method, we will enter the public object intercept (object proxy, method, object [] args, methodproxy, methodproxy) method of dynamic advised interceptor ():
image-20211128125711187
It calls advisedsupport Getinterceptorsanddynamicinception advice(), here is the get call chain. The method userservice without @ Transactional annotation Testtransaction() can't get the proxy call chain at all. The method of the original class is called.
To proxy a method in spring, aop is used. It certainly needs an identification to identify which method or class needs to be proxied. Spring defines @ Transactional as the cut-off point. If we define this identification, it will be proxied.
When is the time for agency?
Spring manages our beans in a unified way. The time of proxy is naturally the process of creating beans. See which class carries this ID and generate proxy objects.
The SpringTransactionAnnotationParser class has a method to judge the TransactionAttribute 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. Multithreaded transaction failure
Assuming that we use transactions in multithreading in the following way, transactions 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 sqlsessions, 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 rational use of transaction nesting
First of all, there is a communication mechanism for transactions:
- REQUIRED (default): the current transaction is supported. If the current transaction does not exist, a new transaction is created. If there is, the current transaction is directly used.
- SUPPORTS: SUPPORTS the use of the current transaction. If the current transaction does not exist, it will not be used.
- MANDATORY: supports the use of the current transaction. If the current transaction does not exist, an Exception will be thrown, that is, it must be in the current 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, suspend the current transaction.
- NEVER: no transaction is executed. If there is currently a transaction, an 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, the performance is similar to REQUIRED.
The default is REQUIRED, that is, calling another transaction in the transaction will not actually recreate the transaction, but reuse the current transaction. So 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 is thrown:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
However, the actual transaction is rolled back normally, and the result is correct. The reason for this problem is that the inner method throws an Exception. It uses the same transaction, indicating that the transaction must be rolled back, but the outer layer is caught. It is the same thing. One says rollback, and one catch es does not let spring perceive exceptions, Isn't that a contradiction? Therefore, spring reported an error and said: this transaction was identified and must be rolled back. Finally, it was rolled back.
How to deal with it
- The outer layer actively throws an error, throw new RuntimeException()
- Use transactionaspectsupport currentTransactionStatus(). setRollbackOnly(); Active identity rollback
@Transactional public void updateUserAge() { try { userMapper.updateUserAge(1); userServiceImpl2.updateUserAge(); }catch (Exception ex){ ex.printStackTrace(); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } }
8. It needs to be considered to rely on external network request rollback
Sometimes, we not only operate our own database, but also need to consider external requests, such as synchronizing data. If synchronization fails, we need to roll back our own state. In this scenario, we must consider whether the network request will make an error, how to deal with the error, and which error code will succeed.
If the network times out, it actually succeeds, but we judge it as unsuccessful and rollback it, which may lead to inconsistent data. This requires the callee to support retry. When retrying, it needs to support idempotent, and the saved state of multiple calls is consistent. Although the whole main process is very simple, there are still many details.
image-20211128153822791
summary
The transaction is wrapped in the complexity of Spring, and many things may have deep source code. When we use it, we should pay attention to simulate and test whether the call can be rolled back normally. It can't be taken for granted that people will make mistakes. Many times, the black box test simply tests this abnormal data. If there is no normal rollback, it needs to be handled 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 go.