[original] 004 | get on the special spring boot transaction weird event analysis vehicle

Keywords: Java Database SpringBoot Spring

Preface

If this is the second time you see the teacher, you are coveting my beauty!

Like + pay attention again, form habit

It doesn't mean anything else. It just needs your camera screen^_^

This series of articles

At present, it's the fourth part of the series. This topic is to explain the spring boot source code in depth. After all, it's source code analysis, which is relatively boring, but it will give you a thorough understanding of boot after reading it! I'll give you an introduction to boot practice. Rest assured. In the first three chapters, you can have a look at those you haven't read.

[original] 001 | take the SpringBoot auto injection source code analysis car

[original] 002 | take the SpringBoot transaction source code analysis car

[original] 003 | get on the real combat vehicle based on SpringBoot business idea

Special vehicle introduction

This special train is the fourth one for Spring Boot transaction weird events, mainly to reproduce and analyze the strange events of transactions.

Special vehicle problem

  • @In the case of multi-threaded access, why does the synchronization method of Transaction annotation still have dirty data?
  • Why does the transaction fail to work when the transaction method is called through this in service?

Special vehicle example

Example 1

Controller code

@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private TestService testService;

    /**
     * @param id
     */
    @RequestMapping("/addStudentAge/{id}")
    public void addStudentAge(@PathVariable(name = "id") Integer id){
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {
                    testService.addStudentAge(id);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

service code

@Service
public class TestService {

    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private TestService testService;
    
    @Transactional(rollbackFor = Exception.class)
    public synchronized void addStudentAge(Integer id) throws InterruptedException {
        Student student = studentMapper.getStudentById(id);
        studentMapper.updateStudentAgeById(student);
    }
}

The sample code is very simple. Start 1000 threads to call the service method. The service first queries the user information from the database, and then + 1 the user's age. The service method has transaction and synchronization features. So let's guess what the final result is?

Example two

Controller code

@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private TestService testService;

    @RequestMapping("/addStudent")
    public void addStudent(@RequestBody Student student) {
        testService.middleMethod(student);
    }
}

service code

@Service
public class TestService {

    @Autowired
    private StudentMapper studentMapper;
    
    public void middleMethod(Student student) {
        // Please note that this is used here
        this.addStudent(student);
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void addStudent(Student student) {
        this.studentMapper.saveStudent(student);
        System.out.println(1/ 0);
    }
}

The sample code is also very simple. First, insert a piece of data into the database, and then output the result of 1 / 0. Then you can guess whether a record will be inserted into the database?

Special vehicle analysis

Example 1 Results

Execution sequence id Name Age
Before execution 10001 xxx 0
After execution 10001 xxx 994

As can be seen from the above database results, when 1000 threads are opened to execute the so-called methods with transaction and synchronization features, there is no 1000 result and dirty data appears.

Analysis of example 1

Let's take a look at the code for example 1

@Service
public class TestService {

    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private TestService testService;
    
    @Transactional(rollbackFor = Exception.class)
    public synchronized void addStudentAge(Integer id) throws InterruptedException {
        Student student = studentMapper.getStudentById(id);
        studentMapper.updateStudentAgeById(student);
    }
}

We can convert the above method into the following one

@Service
public class TestService {

    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private TestService testService;
    
    // Transaction aspect, open transaction
    public synchronized void addStudentAge(Integer id) throws InterruptedException {
        Student student = studentMapper.getStudentById(id);
        studentMapper.updateStudentAgeById(student);
    }
    // Transaction facet, commit or roll back transaction
}

Through the transformation, we can clearly see that the lock will be released after the method is executed. At this time, before the transaction can be submitted, the next request will come in. What we read is the result before the last transaction is submitted, which will lead to the appearance of the final dirty data.

Example 1 solution

The key to solve this problem is that we need to release the lock after the transaction is completed, which can ensure that the previous request is actually completed, including that the next request is allowed to execute after the transaction is submitted, and can ensure the correctness of the result.

Resolve sample code

@RequestMapping("/addStudentAge1/{id}")
public void addStudentAge1(@PathVariable(name = "id") Integer id){
    for (int i = 0; i < 1000; i++) {
        new Thread(() -> {
            try {
                synchronized (this) {
                    testService.addStudentAge1(id);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

As you can see, the lock code contains the transaction code, which can ensure that the transaction is completed before releasing the lock.

Example 1 solution results

Execution sequence id Name Age
Before execution 10001 xxx 0
After execution 10001 xxx 1000

You can see that the results in the database are consistent with what we want.

Example 2 Results

Execution sequence id Name Age
Before execution 10001 xxx 1000
After execution 66666 transaction 22

It can be seen that even if the executed code has transaction characteristics and the error reporting code is executed in the transaction method, a piece of data is finally inserted into the database, which is completely inconsistent with the transaction characteristics.

Example 2 Analysis

Let's look at the code of example 2

@Service
public class TestService {

    @Autowired
    private StudentMapper studentMapper;
    
    public void middleMethod(Student student) {
        // Please note that this is used here
        this.addStudent(student);
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void addStudent(Student student) {
        this.studentMapper.saveStudent(student);
        System.out.println(1/ 0);
    }
}

It can be seen that the middleMethod method calls other transaction methods through this, that is, ordinary calls between methods, without any agent, or transaction characteristics. So in the end, even if the method reports an error, a record is inserted into the database, because although the method is annotated by @ Transactional annotation, it does not have the function of transaction.

Example 2 solution

The solution is simple, replacing this with a proxy object

public void middleMethod1(Student student) {
    testService.addStudent(student);
}

Because testService object is the proxy object, when the method of the proxy object is called, a callback will be executed to open the transaction, execute the target method, commit or rollback the transaction in the callback.

Example 2 solution results

Execution sequence id Name Age
Before execution 10001 xxx 1000

You can see that no new records are inserted into the database, indicating that our service method has the characteristics of transaction.

Special vehicle summary

Studying @ Transactional source code is not only to understand how transactions are implemented, but also to help us quickly locate the source of the problem and solve the problem.

Special vehicle review

Let's review the first two questions:

  • @In the case of multi-threaded access, why does the synchronization method of Transaction annotation still have dirty data? Because the Transaction is outside the lock, the lock is released and the Transaction has not yet been committed. The solution is to let the lock wrap the Transaction and release the lock after the Transaction is completed.
  • Why does the transaction fail to work when the transaction method is called through this in service? Because this refers to the current object, which is just a normal call as seen in the method, and cannot turn on the transaction feature. We all know that transactions are implemented through agents, so we need to use the proxied object to call methods in the service to enable the transaction feature.

Last

Master, master [Java advanced architect], has gained 15W + programmer's attention in each major platform in just one year, focusing on sharing 20 advanced architecture topics such as Java advanced, architecture technology, high concurrency, microservice, BAT interview, redis topic, JVM tuning, Springboot source code, mysql optimization, etc.

Posted by bubblewrapped on Wed, 11 Dec 2019 19:43:06 -0800