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 exampleA
, if you have Yuan at theB
beginning , 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.500
A
B
100
A
100
B
100
A
B
- 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 ,A
and 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.B
500
1000
A
B
100
A
400
B
600
1000
- 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 update
user 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/getUserList
all users and see:
When calling the update interface, the page throws an error:
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/getUserList
and see that both data 11
indicate 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 Mysql
there is actually a concept of a database engine that we can show engines
use to view Mysql
the supported data engines:
You can see Transactions
that column, that 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'
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
So what if we change the engine of the table to something MyISAM
like 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:
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 InnoDB
engine for the transaction to take effect.
2. Methods cannot be private
A transaction must be a public
method. If it is used in a 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 annotation of the transaction must be rewritten. The private
method is It cannot be rewritten, so an error is reported.
The same final
modified method, if annotated, will also report an error, because final
it does not want to be rewritten:
Spring
It mainly uses Bean
the annotation information obtained by radiation, and then uses the dynamic proxy technology AOP
to encapsulate the entire transaction. In theory, I think private
there is no problem in calling the method. method.setAccessible(true);
It can be used at the method level, but the Spring
team may feel that private
the 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.
Protected
Is 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 protected
method, and an error will be reported directly, and it must be used in the same package. We put the controller
sum in service
the same package:
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 public
methods, not in private
, final
, static
methods, otherwise it will not take effect.
3. The exception must be a runtime exception
Springboot
When managing exceptions, only the runtime exception ( RuntimeException
and 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 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
, Error
but error messages. Generally, some uncontrollable errors have occurred in the program, such as no such file, memory overflow, and IO
sudden errors. On the other Exception
hand, except RuntimeException
, everything else CheckException
is an exception that can be handled. The Java
program 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 IOException
IO exceptions, I NoSuchMethodException
did not find this method, I ClassNotFoundException
did not find this class, but RunTimeException
there 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
- The method needs to be used
@Transactional
to start the transaction - When configuring multiple data sources or multiple transaction managers, pay attention to the transactions
A
that cannot be used if the database is operatedB
. Although this problem is very naive, sometimes it is difficult to find the problem with the wrong use. - If in
Spring
, you need to configure@EnableTransactionManagement
to open the transaction, which is equivalent to the configurationxml
file*<tx:annotation-driven/>*
, but itSpringboot
is no longer needed in the in, inspringboot
theSpringBootApplication
annotation contains the@EnableAutoConfiguration
annotation, it will be injected automatically.
@EnableAutoConfiguration
What 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.factories
There 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 @Transactional
addition to being used for methods, it can also be used for classes, indicating that all public
methods 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 service
transaction method to:
public void testTransaction(){
updateUserAge();
}
@Transactional
public void updateUserAge(){
userMapper.updateUserAge(1);
int i = 1/0;
userMapper.updateUserAge(2);
}
In controller
it, 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?
Spring
The 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 DynamicAdvisedInterceptor
enter public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)()
:
It is called inside AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice()
, and here is the call chain to get the call. For methods without @Transactional
annotations userService.testTransaction()
, the proxy call chain cannot be obtained at all, and the methods of the original class are still called.
spring
If you want to proxy a method aop
, you must use an identifier to identify which method or class needs to be proxied. It spring
is defined @Transactional
as a pointcut, and if we define this identifier, it will be proxied.
When is the time to be an agent?
Spring
We have unified management bean
, and the timing of the proxy is naturally bean
the process of creation. To see which class has this logo, the proxy object will be generated.
SpringTransactionAnnotationParser
This class has a method that is used to determine TransactionAttribute
the 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 thrownException
, 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 transactionException
. -
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 REQUIRED
that 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 catch
n’t that a contradiction? So the error says: This transaction is marked and must be rolled back, and it is eventually rolled back .spring
Exception
spring
How to deal with it?
-
- The outer layer actively throws an error,
throw new RuntimeException()
- The outer layer actively throws an error,
-
- Rollback with
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
Active Identity
- Rollback with
@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.
Summarize
Transactions are Spring
wrapped 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.
【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
, LeetCode
etc., 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