1. Transaction relevance

Scenario: When we are developing enterprise applications, unexpected problems may occur online during the sequential execution of data operations. Exceptions may occur in any step of operations, and subsequent operations cannot be completed. In this case, the operation of the database before is not reliable because the service logic is not completed correctly. In this case, the data needs to be rolled back.

The role of transaction is to ensure that every operation of the user is reliable, every step of the operation in the transaction must be successfully executed, as long as there is an exception, it will be back to the state that the operation was not performed at the beginning of the transaction. This is quite understandable. Transfer, ticket purchase, etc., must be completed in the whole process of the event before the event can be successfully executed. Money cannot be transferred to half of the event.

Transaction management is one of the most commonly used functions in the Spring Boot framework. In the actual application development, we basically need to add transaction when processing business logic in the service layer. Of course, sometimes due to the need of the scene, we do not need to add transaction (for example, we need to insert data into a table without mutual influence. Insert as much as you want, you can’t roll back all inserts because some data hangs.

2. Spring Boot transaction configuration

2.1 Dependency Import

To use transactions in Spring Boot, you need to import mysql dependencies:

<dependency>
 <groupId>org.mybatis.spring.boot</groupId>
 <artifactId>mybatis-spring-boot-starter</artifactId>
 <version>1.3.2</version>
</dependency>
Copy the code

Import the mysql dependence, Spring inject DataSourceTransactionManager Boot automatically, we don’t need any other configuration can use @ the use of Transactional annotation for the transaction. As for the configuration of Mybatis, it has been explained in the last class, so it is ok to use the configuration of Mybatis in the last class.

2.2 Testing of transactions

We start by inserting a single entry into the database table:

id user_name password
1 Zhang SAN 123456

Then we write an inserted mapper:

public interface UserMapper {
    @Insert("insert into user (user_name, password) values (#{username}, #{password})")
    Integer insertUser(User user);
}
Copy the code

In the Service layer, we manually throw an exception to simulate the actual exception, and then observe whether the transaction is rolled back. If there are no new records in the database, the transaction is successfully rolled back.


@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    @Transactional
    public void isertUser(User user) {
        // Insert user information
        userMapper.insertUser(user);
        // Manually throw an exception
        throw newRuntimeException(); }}Copy the code

Let’s test it out:

@RestController
public class TestController {

    @Resource
    private UserService userService;

    @PostMapping("/adduser")
    public String addUser(@RequestBody User user) throws Exception {
        if (null! = user) { userService.isertUser(user);return "success";
        } else {
            return "false"; }}}Copy the code

We use Postman to call this interface, because an exception was thrown in the program, which caused the transaction to roll back. We refresh the database, and no record is added, indicating that the transaction is effective. Transactions are very simple and we usually don’t have many problems when we use them, but it’s not just that…

3. Summary of common problems

As you can see, it’s easy to use transactions in Spring Boot. The @Transactional annotation solves the problem, so to speak. But in real projects, there are many holes that we don’t notice when we write code. In addition, it is not easy to find these small pits under normal circumstances. When the project is written large, one day something suddenly goes wrong, and it is very difficult to troubleshoot the problems. It will definitely be blind at that time, and a lot of energy is needed to troubleshoot the problems.

In this section, I specifically summarize the details related to transactions that often appear in actual projects. I hope readers can benefit from them after reading them and implement them into their own projects.

3.1 The exception was not “caught”

The first thing to say is that the exception was not “caught”, resulting in the transaction not being rolled back. We may have already considered the existence of an exception in our business code, or the editor may have prompted us to throw an exception, but there is a point to note: it does not mean that we throw an exception and the transaction will be rolled back. Let’s look at an example:

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;
    
    @Override
    @Transactional
    public void isertUser2(User user) throws Exception {
        // Insert user information
        userMapper.insertUser(user);
        // Manually throw an exception
        throw new SQLException("Database exception"); }}Copy the code

In this method, if an exception is thrown, the transaction should be rolled back. This is not the case. Readers can use the controller interface in my source code. A postman test shows that it is still possible to insert a piece of user data.

So what’s the problem? Spring Boot’s default transaction rule is to roll back a RuntimeException or Error. For example, the RuntimeException thrown in our example above is fine, but the SQLException thrown is not rolled back. For non-transactional exceptions, use the rollbackFor attribute in the @Transactional annotation to specify exceptions such as @Transactional(rollbackFor = exception.class), Then there are no problems, so in a real project, be sure to specify exceptions.

3.2 Exceptions are “eaten”

That’s a funny title. How does an anomaly get eaten? Back in the real world, when we handle an exception, we can either throw it out and let the next layer catch and handle it; You can either try catch the exception, get rid of the exception where it occurs. Because of this try… Catch, so the exception is “eaten” and the transaction cannot be rolled back. Let’s look at the example above, with a simple change in the code:

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void isertUser3(User user) {
        try {
            // Insert user information
            userMapper.insertUser(user);
            // Manually throw an exception
            throw new SQLException("Database exception");
        } catch (Exception e) {
   // Exception handling logic}}}Copy the code

The reader can use the Controller interface in my source code to test postman and see that it is still possible to insert a piece of user data to show that the transaction has not been rolled back because of an exception thrown. This detail is often harder to find than the pit above, because our minds tend to lead to try… Once the problem of catch code occurs, it is often difficult to troubleshoot. Therefore, when writing code, we must think more and pay more attention to such details, so as to avoid burying holes for ourselves.

So how do we solve this? Just throw it up and give it to the next level. Don’t eat the exception yourself in the transaction.

3.3 Scope of transactions

Transaction scope this thing buries deeper than the above two pits! I also write this, because this is before I met in the actual project, the scenario I wouldn’t have simulated in this course, I wrote a demo to let everyone see, remember, the pit when writing code, after concurrent problems, will pay attention to the pit, so this class would be valuable.

Let me write a demo:

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public synchronized void isertUser4(User user) {
        // Actual business......userMapper.insertUser(user); }}Copy the code

As you can see, I’ve added the synchronized keyword to the methods of the business-layer code because of concurrency concerns. Let me give you a practical scenario, for example, in a database, there is only one record for a certain user, and when the next insert action comes, it will first check whether there is the same user in the database. If there is, it will not insert, it will update, and if there is not, it will insert. Therefore, theoretically, there is always the same user information in the database. There will be no information about two identical users inserted into the same database.

But in the pressure test, there will be the above problem, the database does have two information of the same user, analysis of its reason, lies in the scope of the transaction and the scope of the lock problem.

As you can see from the above method, the transaction is started at the beginning of the execution of the method and closed at the end of the execution. Synchronized doesn’t work, however, because the scope of the transaction is larger than the scope of the lock. In other words, the lock is released after the part of the code that locked the database is executed, but the transaction is not completed. Another thread enters the database before the transaction is completed, and the second thread enters the database in the same state as the first thread. The mysql Innodb engine’s default isolation level is repeatable reads (in the same transaction, the result of the SELECT is the state at the start of the transaction), thread 2 transaction starts, thread 1 has not completed the commit, so the read data has not been updated. The second thread also inserts, resulting in dirty data.

This problem can be avoided by first removing the transaction (not recommended); Second, place a lock where the service is called, ensuring that the scope of the lock is larger than the scope of the transaction.

4. To summarize

This chapter summarizes how to use transactions in Spring Boot. It is easy to use the @Transactional annotation. In addition, it focuses on the summary of three possible pit points in the actual project, which is very meaningful, because it is good that the transaction does not have a problem, but it is difficult to troubleshoot the problems, so the summary of the three points for attention, I hope to help friends in the development.