1. Preface
Hello friends, I am a beer bear. Today I want to talk to you about some pits involving database transactions in Spring
Spring
provides developers with the use of declarative transaction , that is, mark the @Transactional
annotation on the method to start the transaction.
We all know that when the business code operates on the data, it must be controlled by the transaction.
For example, when writing a business code for a merchant to sell something, the logic of the code is that the merchant first generates an order (the order information is inserted into the database), and then the money is transferred to its own account (the money in the database is increased). If the latter operation fails, then the former must not be inserted successfully. At this time, the rollback of the transaction will be used.
Although most of the back-end development students have this concept, some errors still occur when @Transactional
code review
on the code written by new students from the company, I saw that they had some incorrect use of the annotations of @Transactional
Spring
project. While correcting their mistakes, I couldn’t help thinking that I had fallen into these pits tooヽ(ー_ー)ノ
So I wanted to make a guide to avoiding pits ~ for the use of this annotation, and share it with the community.
This article will introduce @Transactional
in normal business development, and give corresponding error code examples. For each type of error, explain the reason and give the correct posture @Transactional
Let's take a look together next!
Two, experiment preparation
2.1 Database
We define a goods_stock
goods inventory table in the database and give some initial data:
Indicates that the current product inventory with product id good_0001 is 10 pieces.
2.2 Spring Boot+Mybatis
In the Java method, we use Mybatis
to reduce the inventory, and mark the annotation of @Transactional
on the method to see if the annotation can invalidate the transaction and roll back if it encounters an error.
The project structure is as follows:
We will use Swagger
call Controller
the layer interface, the interface call Service
layer GoodsStockServiceImp
specific service code, i.e. reduction of inventory operations.
The specific sql
executes the inventory minus 10 operation, that is, if the business code is executed successfully, the product inventory becomes 0:
<?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.beerbear.springboottransaction.dao.GoodsStockMapper">
<update id="updateStock">
update goods_stock set stock = stock - 10
</update>
</mapper>
Three, throw an exception
3.1 The method marked by @Transactional is not out of exception propagation
Many times, in real business development, it is always hoped that the interface can return a fixed class instance-this is called unified return result. In this article, the Result
class is used as the unified return result. For details, please refer to the code attached to this article.
Therefore, it is possible to directly Result
class object in the method of Service
for convenience. In order to avoid being affected by exceptions and failing to return the result set, the try-catch
will be used. When an error occurs in the business code and an exception is thrown, it will be caught For this exception, write the exception information into the relevant field of the Result and return it to the caller. An example of this type is given below:
Controller
:
@Controller
@RestController
@Api( tags = "测试事务是否生效")
@RequestMapping("/test/transactionalTest")
@Slf4j
public class GoodsStockController {
@Autowired
private GoodsStockService goodsStockService;
/**
* create by: Beer Bear
* description: 第一个方法。
* create time: 2021/7/25 21:38
*/
@GetMapping("/exception/first")
@ApiOperation(value = "关于异常的第一个方法,不能够回滚", notes = "因为异常未能被事务发现,所以没有回滚")
@ResponseBody
public Result firstFunctionAboutException(){
try{
return goodsStockService.firstFunctionAboutException();
}catch (Exception e){
return Result.server_error().Message("操作失败:"+e.getMessage());
}
}
}
Service
in 06108e2761b199:
@Autowired
private GoodsStockMapper goodsStockMapper;
@Override
@Transactional
public Result firstFunctionAboutException() {
try{
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new RuntimeException();
return Result.ok();
}catch (Exception e){
log.info("减库存失败!" + e.getMessage());
return Result.server_error().Message("减库存失败!" + e.getMessage());
}
}
In firstFunctionAboutException
method try
block of code will throw an RuntimeException
unusual, but if so it can be rolled back? Let's take a look at it through experiments:
Use Swagger
call the interface:
After calling the interface, it stands to reason that the transaction should be rolled back, and the inventory quantity will not become 0, but the result is:
length, these screenshots will no longer appear in the following, but will be replaced by text
Obviously the transaction was not rolled back. We all know that when an error occurs during program execution and an exception is thrown, the transaction will roll back. Although the exception occurred here, it was digested by the method itself (catch dropped), and the exception was not discovered by the transaction, so this is There will be no rollback.
Let us give the relevant right solution - will service
in try-catch
statement removed:
@Override
@Transactional
public void secondFunctionAboutException() {
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new RuntimeException();
}
In this way, the transaction can be rolled back. (But in this case, what to do with the exception? You can't report an exception directly. It's very simple, just put the exception on the Controller layer to handle it.)
Here is a summary of the first pit pit avoidance guide:
When @Transactional
, if the exception is not propagated outside the method, the transaction will not be rolled back; on the contrary, only if the exception propagates outside the method, the transaction will roll back.
3.2 Obviously throws an exception but does not roll back
Now we all know that when an error occurs during program execution and an exception is thrown, as long as you don't handle the exception, let the exception break @Transactional
, and you can achieve the desired rollback.
But is this really the case? Let's look at another case below:
@Override
@Transactional
public void thirdFunctionAboutException() throws Exception {
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new Exception();
}
In fact, the transaction in this method will not be rolled back.
This is also the most common mistake we make in actual development. We think that only if an exception is thrown, it will definitely roll back, and the reality is slapped in the face.
But I don't think this is a shame, because when we go to use a tool, we may not have the energy and ability to learn some of its principles at the beginning, and thus fall into some pits that we are not easy to find. As long as you continue to study later, you will surely fill up these pits slowly, and you will become stronger and stronger.
Okay, let's get back to business, why won't this transaction roll back? We compare this method with secondFunctionAboutException
above and find that it is only the difference between RuntimeException
and Exception
Indeed it is, is because Spring
of @Transactional
comment is default only when thrown RuntimeException
runtime exception, will be rolled back.
Spring
usually uses RuntimeException
indicate an unrecoverable error condition. In other words, for other exceptions, Spring
feels that it doesn't matter, so it doesn't roll back.
Below I give two solutions:
@Override
@Transactional
public void thirdFunctionAboutException1(){
try{
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new Exception();
}catch (Exception e){
log.info("出现异常"+e.getMessage());
throw new RuntimeException("手动抛出RuntimeException");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void thirdFunctionAboutException2() throws Exception {
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new Exception();
}
The first is to manually throw the RuntimeException
exception, and the second is to change the default @Transactional
rollback exception setting ( RuntimeException
inherits the Exception
exception).
@Transactional(rollbackFor = Exception.class)
Here is a summary of the second pit pit avoidance guide:
By default, if the exception we throw is notRuntimeException
, the transaction will still not roll back; you need to manually throw theRuntimeException
exception or change the default configuration of @Transactional in Spring.
Fourth, the transaction still does not take effect
Even if we notice the @Transactional
and avoid these pits correctly, we will still fall into some pits that are more difficult to find and understand. In this section, we will continue to cite counter-examples, and explain the reasons why the transactions did not take effect in these examples and give solutions. In this section, you will also learn about the @Transactional
affairs and Spring AOP
.
4.1 Example 1
Add these two methods in service
@Override
public void privateFunctionCaller (){
privateCallee();
}
@Transactional
private void privateCallee(){
goodsStockMapper.updateStock();
throw new RuntimeException();
}
In Controller
call service
of privateFunctionCaller
method indirectly calls marked @Transactional
annotated method privateCallee
.
After executing the code, it was found that the transaction was not rolled back. what about this?
We Service
marked on the class of @Service
notes represent the class as Bean
injection AOP
container, and Spring
is achieved through dynamic proxy AOP
of. Also said AOP
container Bean
actually a proxy object.
Spring
also supports @Transactional
in this way Spring
encapsulates the method in the original object (that is, when the method marked with this annotation is checked, it will add a transaction).
This behavior is called enhancement for the target method. Although Spring's way of implementing dynamic proxy is CGLIB
, I want to explain it here with the implementation of JDK dynamic proxy, because it is easier to understand.
It service.function()
that if the agent is enhanced, then function
must not be private. Therefore private
does not take effect, and naturally it cannot be rolled back.
In fact, when you write the above-mentioned code, if the compiler you use is IDEA, the compiler will prompt an error, of course, it will only report a red code, and will not affect the compilation and execution.
There are JDK implementation and CGLIB among the ways to implement dynamic proxy in Java. Students who do not understand dynamic agents can learn about the agent model and the implementation of MaBtais in Spring.
4.2 Example 2
That we are not as long as the private
into public
can it? The following code shows that many students often fall into a pit when they @Transactional
@Override
public void publicFunctionCaller (){
publicCallee();
}
@Override
@Transactional
public void publicCallee(){
goodsStockMapper.updateStock();
throw new RuntimeException();
}
We Controller
call Service
in publicFunctionCaller
, we found that the transaction is not rolled back, this is why?
As we mentioned above, in Controller
, the injected Service
object is actually his proxy object. When the publicCallee
method is called, there is no @Transactional
annotation on it.
So simply execute service.function()
, that is, in the method publicFunctionCaller
of the proxy object, the original object of Service first calls its own publicFunctionCaller
method, and then it calls its own publicCallee
method. publicCallee
method that has been enhanced by the proxy object (with transactions) will not be taken at all. Natural transactions will not roll back.
Solution, I think we will be able to find their own, that is in Controller
by injection service
of bean
call directly marked @Transactional
method, for example, earlier in secondFunctionAboutException
is called.
Of course, we can also save the country with the
service
, so that we can implement proxy objects to call enhanced methods:
@Override
@Transactional
public void publicCallee(){
goodsStockMapper.updateStock();
throw new RuntimeException();
}
@Autowired
private GoodsStockService self;
@Override
public void aopSelfCaller (){
self.publicCallee();
}
But obviously this does not conform to the hierarchical structure and is not elegant.
Here is a summary of the third pit pit avoidance guide:
The method marked with the @Transactioal
annotation must be public and must be directly called by the injected bean to roll back the transaction.
So far, @Transactional
is over. If you have any questions, please comment and leave a message. Let's communicate with each other.
I also hope that everyone likes more and will continue to output more high-quality articles in the future!
All the code in this article, put it on Gitee 16108e2761bfe3, and those who need it will
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。