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 scenarios of various affairs. We thought we might as well make a summary, so that we can have confidence 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 (atomicity): the operations included in the transaction are either all successful or all failed. There will be no intermediate state of half success and half failure. For example, if A and B have 500 yuan at the beginning and A transfers 100 to B, then A's money is 100 less and B's money must be 100 more. If A has less money and B has not received money, then 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 for 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 of 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, the basic unit of concurrency control, ensuring the results of operations, and distributed transactions. However, 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 state. 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 article is also based on the previous environment:
- JDK 1.8
- Maven 3.6
- Docker
- Mysql
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:
When calling the update interface, the page throws an error:
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 the data is both 11, indicating 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:
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
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:
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 transaction annotation must be rewritable, and the private method cannot be rewritten, so an error is reported.
The same final modification method, if annotated, will also report an error, because using final does not want to be rewritten:
Spring mainly uses radiation to obtain Bean annotation information, and then uses AOP based on dynamic proxy technology to encapsulate the whole transaction. In theory, I think there is no problem calling private method. 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:
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 will only roll back the runtime exceptions (RuntimeException and its subclasses). For example, i=1/0; written earlier will generate runtime exceptions.
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, and Error is an 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, 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
The default transaction rollback is: 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. 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, B transactions 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 configuration is available 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, indicating 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 wraps the cut surface method 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 dynamicadisceptor ():
It calls advised support 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. An identifier must be used to identify which method or class needs to be proxied. Spring defines @ Transactional as the cut-off point. If we define this identifier, 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. Transaction failure under multithreading
Assuming that we use transactions in multithreading as follows, 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, 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 it does, 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, it will follow ` REQUIRED
Not much.
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 reports an error, saying that the transaction has been identified and must be rolled back. Finally, it is rolled back.
How to deal with it?
-
- The outer layer actively throws an error, throw new RuntimeException()
-
- Use transactionaspectsupport currentTransactionStatus(). setRollbackOnly(); Proactive identity rollback
@Transactional public void updateUserAge() { try { userMapper.updateUserAge(1); userServiceImpl2.updateUserAge(); }catch (Exception ex){ ex.printStackTrace(); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } }
8. External network dependent request rollback needs to be considered
Sometimes, we not only operate our own database, but also consider external requests, such as synchronizing data. If synchronization fails, we need to roll back our 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. The callee needs to support retry. When retrying, it needs to support idempotent. Multiple calls save the same state. Although the whole main process is very simple, there are still many details.
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.
[about the author]:
Qin Huai, official account [Qinhuai grocery store] the author, the road of technology is not at the moment. The mountain is high and the river is long. Even if it is slow, it will not stop. His personal writing direction: Java source code analysis, JDBC, Mybatis, Spring, redis, distributed, sword finger Offer, LeetCode, etc. He carefully writes every article. He doesn't like the title party and fancy. He mostly writes a series of articles, which can't guarantee that I write completely correctly Yes, but I guarantee that what I write has been practiced or searched for information. Please correct any omissions or mistakes.