3
头图

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ヽ(ー_ー)ノ

img

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!

img

Two, experiment preparation

2.1 Database

We define a goods_stock goods inventory table in the database and give some initial data:

image-20210801102106697

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:

image-20210801105126126

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:

image-20210801110934089

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:

image-20210801112221671

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.

img

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 not RuntimeException , the transaction will still not roll back; you need to manually throw the RuntimeException 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.

image-20210801175619550

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.

image-20210801180654695

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?

35301CCAEFC02FBE42F746B3969B233F.png

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.

img

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!

img

All the code in this article, put it on Gitee 16108e2761bfe3, and those who need it will


BeerBear啤酒熊
241 声望16 粉丝

Stay hungary, Stay foolish.