preface
For students engaged in java development, spring affairs must be familiar.
In some business scenarios, the data of multiple tables needs to be written simultaneously in one request. In order to ensure the atomicity of operations (success or failure at the same time) and avoid data inconsistency, we generally use spring transactions.
Indeed, spring transactions are fun to use. You can easily handle transactions with a simple annotation: @ Transactional. I guess most of my friends use it like this, and it's always cool.
But if you don't use it properly, it will also pit you.
Today, let's talk about some scenarios of transaction failure. Maybe you've been caught. No, let's have a look.
1. The transaction does not take effect
1. Access rights
As we all know, there are four kinds of access permissions for java: private, default, protected and public. Their permissions increase from left to right.
However, if we define some transaction methods with wrong access rights in the development process, it will lead to problems in transaction functions, such as:
@Service public class UserService {<span class="token annotation punctuation">@Transactional</span> <span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">add</span><span class="token punctuation">(</span><span class="token class-name">UserModel</span> userModel<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> <span class="token function">saveData</span><span class="token punctuation">(</span>userModel<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">updateData</span><span class="token punctuation">(</span>userModel<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
}
We can see that the access permission of the add method is defined as private, which will lead to transaction failure. spring requires that the proxy method must be public.
To put it bluntly, there is a judgment in the computeTransactionAttribute method of AbstractFallbackTransactionAttributeSource class. If the target method is not public, the TransactionAttribute returns null, that is, transactions are not supported.
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) { // Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; }<span class="token comment">// The method may be on an interface, but we need attributes from the target class.</span> <span class="token comment">// If the target class is null, the method will be unchanged.</span> <span class="token class-name">Method</span> specificMethod <span class="token operator">=</span> <span class="token class-name">AopUtils</span><span class="token punctuation">.</span><span class="token function">getMostSpecificMethod</span><span class="token punctuation">(</span>method<span class="token punctuation">,</span> targetClass<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// First try is the method in the target class.</span> <span class="token class-name">TransactionAttribute</span> txAttr <span class="token operator">=</span> <span class="token function">findTransactionAttribute</span><span class="token punctuation">(</span>specificMethod<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>txAttr <span class="token operator">!=</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> <span class="token keyword">return</span> txAttr<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">// Second try is the transaction attribute on the target class.</span> txAttr <span class="token operator">=</span> <span class="token function">findTransactionAttribute</span><span class="token punctuation">(</span>specificMethod<span class="token punctuation">.</span><span class="token function">getDeclaringClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>txAttr <span class="token operator">!=</span> <span class="token keyword">null</span> <span class="token operator">&&</span> <span class="token class-name">ClassUtils</span><span class="token punctuation">.</span><span class="token function">isUserLevelMethod</span><span class="token punctuation">(</span>method<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> <span class="token keyword">return</span> txAttr<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>specificMethod <span class="token operator">!=</span> method<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> <span class="token comment">// Fallback is to look at the original method.</span> txAttr <span class="token operator">=</span> <span class="token function">findTransactionAttribute</span><span class="token punctuation">(</span>method<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>txAttr <span class="token operator">!=</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> <span class="token keyword">return</span> txAttr<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">// Last fallback is the class of the original method.</span> txAttr <span class="token operator">=</span> <span class="token function">findTransactionAttribute</span><span class="token punctuation">(</span>method<span class="token punctuation">.</span><span class="token function">getDeclaringClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>txAttr <span class="token operator">!=</span> <span class="token keyword">null</span> <span class="token operator">&&</span> <span class="token class-name">ClassUtils</span><span class="token punctuation">.</span><span class="token function">isUserLevelMethod</span><span class="token punctuation">(</span>method<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> <span class="token keyword">return</span> txAttr<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
In other words, if the access permission of our customized transaction method (i.e. target method) is not public but private, default or protected, spring will not provide transaction function.
2. The method was modified with final
Sometimes, when a method does not want to be subclassed, it can be defined as final. It is OK to define a normal method in this way, but if the transaction method is defined as final, for example:
We can see that the add method is defined as final, which will lead to transaction invalidation.
Why?
If you have seen the source code of spring transaction, you may know that aop is used at the bottom of spring transaction, that is, through jdk dynamic agent or cglib, it helps us generate the agent class and realize the transaction function in the agent class.
However, if a method is decorated with final, the method cannot be overridden and transaction functions can be added in its proxy class.
Note: if a method is static, it cannot become a transaction method through dynamic proxy.
3. Method internal call
Sometimes we need to call another transaction method in a method in a Service class, for example:
We can see that in the transaction method add, the transaction method updateStatus is called directly. As can be seen from the previous introduction, the updateStatus method has the ability to generate transactions because spring aop generates proxy objects, but this method directly calls the method of this object, so the updateStatus method will not generate transactions.
It can be seen that direct internal call of methods in the same class will lead to transaction failure.
So the question is, what if some scenarios really want to call another method in one way of the same class?
3.1 add a new Service method
This method is very simple. You only need to add a new Service method, add the @ Transactional annotation to the new Service method, and move the code that needs transaction execution to the new method. The specific codes are as follows:
3.2 inject yourself into this Service class
If you don't want to add a new Service class, injecting yourself into the Service class is also an option. The specific codes are as follows:
Some people may have this question: will this approach lead to circular dependency?
Answer: No.
In fact, the three-level cache inside spring ioc ensures that it will not have circular dependency. But there are some holes. If you want to know more about circular dependency, you can see my previous article< spring: how do I solve circular dependency?>.
3.3 passing AopContent class
Use aopcontext. In this Service class Currentproxy() get proxy object
The above method 2 can indeed solve the problem, but the code does not look intuitive. You can also use AOPProxy to obtain the proxy object in the Service class to achieve the same function. The specific codes are as follows:
4. Not managed by spring
In our usual development process, one detail is easy to be ignored. That is, the premise of using spring transactions is: to be managed by spring, you need to create bean instances.
Generally, we can automatically implement the functions of bean instantiation and dependency injection through @ Controller, @ Service, @ Component, @ Repository and other annotations.
Of course, there are many ways to create bean instances. Interested partners can take a look at another article I wrote earlier< @Do you know all these operations of Autowired?>
If one day, you rush to develop a Service class, but forget to add @ Service annotation, such as:
From the above example, we can see that if the UserService class is not annotated with @ Service, the class will not be managed by spring, so its add method will not generate transactions.
5. Multithreaded call
In the actual project development, there are many usage scenarios of multithreading. Is there a problem if spring transactions are used in a multithreaded scenario?
From the above example, we can see that the transaction method add calls the transaction method doOtherThing, but the transaction method doOtherThing is invoked in another thread.
In this way, the two methods are not in the same thread, and the database connections obtained are different, but two different transactions. If you want to throw an exception in the doOtherThing method, it is impossible for the add method to roll back.
If you have seen the source code of spring transaction, you may know that spring transaction is realized through database connection. A map is saved in the current thread. key is the data source and value is the database connection.
When we talk about the same transaction, we actually mean the same database connection. Only with the same database connection can we commit and rollback at the same time. If you are in different threads, the database connections you get must be different, so they are different transactions.
6. Table does not support transactions
As we all know, before mysql5, the default database engine was myisam.
Needless to say, its advantages: index files and data files are stored separately. For single table operations with more queries and less writes, the performance is better than innodb.
Some old projects may still use it.
When creating a table, you only need to set the ENGINE parameter to MyISAM:
myisam is easy to use, but one fatal problem is that it does not support transactions.
If only the single table operation is good, there will be no big problems. However, if you need to operate across multiple tables, the data is likely to be incomplete because it does not support transactions.
In addition, myisam does not support row locks and foreign keys.
Therefore, in the actual business scenario, myisam is not used much. After mysql5, myisam has gradually withdrawn from the stage of history and replaced innodb.
Sometimes in the process of development, we find that the transaction of a table has not been effective, which is not necessarily the pot of spring transaction. It's best to confirm whether the table you use supports transactions.
7. Transaction not opened
Sometimes, the root cause of a transaction not taking effect is that the transaction is not opened.
You may find this sentence funny.
Isn't starting a transaction the most basic function in a project?
Why is the transaction not started?
Yes, if the project has been built, there must be transaction functions.
However, if you are building a project demo, there is only one table, and the transactions of this table do not take effect. So what causes it?
Of course, there are many reasons, but the reason why the transaction is not started is extremely easy to be ignored.
If you're using the springboot project, you're lucky. Because springboot has silently started the transaction for you through the DataSourceTransactionManagerAutoConfiguration class.
All you need to do is to configure spring Datasource related parameters are sufficient.
However, if you are still using a traditional spring project, you need to use ApplicationContext In the XML file, manually configure transaction related parameters. If you forget the configuration, the transaction will not take effect.
The specific configuration information is as follows:
In silence, if the pointcut matching rule in the pointcut tag is mismatched, some transactions will not take effect.
Second, the transaction is not rolled back
1. Error propagation characteristics
In fact, when using @ Transactional annotation, we can specify the propagation parameter.
This parameter is used to specify the propagation characteristics of transactions. spring currently supports seven propagation characteristics:
- REQUIRED if there is a transaction in the current context, join the transaction. If there is no transaction, create a transaction, which is the default propagation attribute value.
- SUPPORTS if there is a transaction in the current context, the transaction can be added to the transaction. If there is no transaction, it can be executed in a non transaction manner.
- MANDATORY if there is a transaction in the current context, otherwise an exception is thrown.
- REQUIRES_NEW will create a new transaction every time, and suspend the transaction in the context at the same time. After the current new transaction is completed, the context transaction will be resumed and then executed.
- NOT_SUPPORTED if there is a transaction in the current context, suspend the current transaction, and then the new method is executed in an environment without transaction.
- NEVER throw an exception if there is a transaction in the current context. Otherwise, execute the code in a transaction free environment.
- NESTED if there is a transaction in the current context, the NESTED transaction will be executed. If there is no transaction, a new transaction will be created.
If we set the propagation property incorrectly when manually setting the propagation parameter, for example:
We can see that the transaction propagation feature of the add method is defined as propagation Never, this type of propagation feature does not support transactions. If there are transactions, exceptions will be thrown.
At present, only these three propagation features can create new transactions: REQUIRED and REQUIRES_NEW,NESTED.
2. He swallowed it himself
The transaction will not be rolled back. The most common problem is that developers manually try... catch exceptions in the code. For example:
In this case, of course, the spring transaction will not be rolled back, because the developer caught the exception and did not throw it manually. In other words, he swallowed the exception.
If you want a spring transaction to roll back normally, you must throw an exception that it can handle. If no exceptions are thrown, spring thinks the program is normal.
3. Throw other abnormalities manually
Even if the developer does not catch the exception manually, if the exception thrown is incorrect, the spring transaction will not be rolled back.
In the above case, the developer caught the Exception himself and threw the Exception manually: Exception, and the transaction will not be rolled back.
Because of spring transactions, only RuntimeException (runtime Exception) and Error (Error) will be rolled back by default. For ordinary Exception (non runtime Exception), it will not be rolled back.
4. Customized rollback exception
When declaring a transaction with @ Transactional annotation, sometimes we want to customize the rollback exception, which spring also supports. You can complete this function by setting the rollbackFor parameter.
However, if the value of this parameter is set incorrectly, some inexplicable problems will arise, such as:
@Slf4j @Service public class UserService {<span class="token annotation punctuation">@Transactional</span><span class="token punctuation">(</span>rollbackFor <span class="token operator">=</span> <span class="token class-name">BusinessException</span><span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">add</span><span class="token punctuation">(</span><span class="token class-name">UserModel</span> userModel<span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token class-name">Exception</span> <span class="token punctuation">{<!-- --></span> <span class="token function">saveData</span><span class="token punctuation">(</span>userModel<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">updateData</span><span class="token punctuation">(</span>userModel<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
If the program reports an error and throws SqlException, DuplicateKeyException and other exceptions when executing the above code to save and update data. BusinessException is our custom exception. The exception reporting an error does not belong to BusinessException, so the transaction will not be rolled back.
Even if there is a default value for rollback for, Alibaba developer specification still requires developers to specify this parameter again.
Why?
Because if the default value is used, once the program throws an Exception, the transaction will not be rolled back, which will cause a big bug. Therefore, it is recommended to set this parameter to Exception or Throwable in general.
5. Too many nested transaction rollbacks
public class UserService {<span class="token annotation punctuation">@Autowired</span> <span class="token keyword">private</span> <span class="token class-name">UserMapper</span> userMapper<span class="token punctuation">;</span> <span class="token annotation punctuation">@Autowired</span> <span class="token keyword">private</span> <span class="token class-name">RoleService</span> roleService<span class="token punctuation">;</span> <span class="token annotation punctuation">@Transactional</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">add</span><span class="token punctuation">(</span><span class="token class-name">UserModel</span> userModel<span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token class-name">Exception</span> <span class="token punctuation">{<!-- --></span> userMapper<span class="token punctuation">.</span><span class="token function">insertUser</span><span class="token punctuation">(</span>userModel<span class="token punctuation">)</span><span class="token punctuation">;</span> roleService<span class="token punctuation">.</span><span class="token function">doOtherThing</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
}
@Service
public class RoleService {
<span class="token annotation punctuation">@Transactional</span><span class="token punctuation">(</span>propagation <span class="token operator">=</span> <span class="token class-name">Propagation</span><span class="token punctuation">.</span>NESTED<span class="token punctuation">)</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">doOtherThing</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"preservation role Table data"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
In this case, nested internal transactions are used, originally hoping to call roleservice If an exception occurs in the doOtherThing method, only the contents in the doOtherThing method will be rolled back, not usermapper Contents in insertUser, that is, rollback savepoint.. But the truth is, insertUser also rolled back.
why?
Because there is an exception in the doOtherThing method and it is not captured manually, it will continue to throw up and catch the exception in the proxy method of the outer add method. Therefore, in this case, the whole transaction is rolled back directly, not just a single savepoint.
How can I rollback savepoints only?
@Slf4j @Service public class UserService {<span class="token annotation punctuation">@Autowired</span> <span class="token keyword">private</span> <span class="token class-name">UserMapper</span> userMapper<span class="token punctuation">;</span> <span class="token annotation punctuation">@Autowired</span> <span class="token keyword">private</span> <span class="token class-name">RoleService</span> roleService<span class="token punctuation">;</span> <span class="token annotation punctuation">@Transactional</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">add</span><span class="token punctuation">(</span><span class="token class-name">UserModel</span> userModel<span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token class-name">Exception</span> <span class="token punctuation">{<!-- --></span> userMapper<span class="token punctuation">.</span><span class="token function">insertUser</span><span class="token punctuation">(</span>userModel<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">try</span> <span class="token punctuation">{<!-- --></span> roleService<span class="token punctuation">.</span><span class="token function">doOtherThing</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> log<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span><span class="token function">getMessage</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
You can put internal nested transactions in try/catch without throwing exceptions up. This ensures that if an exception occurs in an internal nested transaction, only the internal transaction will be rolled back without affecting the external transaction.
Three others
1 major business issues
When using spring transactions, there is a very troublesome problem, that is, the big transaction problem.
Usually, we will annotate @ Transactional on the method and add transaction functions, such as:
@Service public class UserService {<span class="token annotation punctuation">@Autowired</span> <span class="token keyword">private</span> <span class="token class-name">RoleService</span> roleService<span class="token punctuation">;</span> <span class="token annotation punctuation">@Transactional</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">add</span><span class="token punctuation">(</span><span class="token class-name">UserModel</span> userModel<span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token class-name">Exception</span> <span class="token punctuation">{<!-- --></span> <span class="token function">query1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">query2</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">query3</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> roleService<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span>userModel<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">update</span><span class="token punctuation">(</span>userModel<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
}
@Service
public class RoleService {
<span class="token annotation punctuation">@Autowired</span> <span class="token keyword">private</span> <span class="token class-name">RoleService</span> roleService<span class="token punctuation">;</span> <span class="token annotation punctuation">@Transactional</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">save</span><span class="token punctuation">(</span><span class="token class-name">UserModel</span> userModel<span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token class-name">Exception</span> <span class="token punctuation">{<!-- --></span> <span class="token function">query4</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">query5</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">query6</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">saveData</span><span class="token punctuation">(</span>userModel<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
However, if the @ Transactional annotation is added to the method, there is a disadvantage that the whole method is included in the transaction.
In the above example, in the UserService class, only these two lines need transactions:
roleService.save(userModel); update(userModel);
- 1
- 2
In the RoleService class, only this row needs transactions:
saveData(userModel);
- 1
The current writing method will cause all query methods to be included in the same transaction.
If there are many query methods, the call level is very deep, and some query methods are time-consuming, the whole transaction will be very time-consuming, resulting in large transaction problems.
About the harm of big business problems, you can read another article of mine< How to solve the big headache problem? >, there are detailed explanations on it.
Recently, I inadvertently got a question brushing note written by the boss of BAT factory, which suddenly opened up my Ren Du pulse, and more and more felt that the algorithm was not as difficult as expected.
The notes written by the BAT boss made me soft on my offer
2. Programming services
All the contents mentioned above are based on @ Transactional annotation, mainly about its transaction problem. We call this kind of transaction: declarative transaction.
In fact, spring also provides another way to create transactions, that is, transactions realized by manually writing code. We call this kind of transaction: programming transaction. For example:
@Autowired private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
In spring, in order to support programmatic transactions, a special class is provided: TransactionTemplate, which implements the function of transactions in its execute method.
Compared with @ Transactional annotation declarative transaction, I suggest you use the programmatic transaction based on TransactionTemplate. The main reasons are as follows:
- Avoid transaction invalidation due to spring aop problems.
- It can control the scope of transactions with smaller granularity and is more intuitive.
It is recommended to use @ transaction annotation less in transactions. However, it does not mean that it must not be used. If some business logic in the project is relatively simple and does not change frequently, it is OK to use @ Transactional annotation to start a transaction, because it is simpler and more efficient, but be careful about the problem of transaction failure.