Explanation of SpringBoot Transaction Control

Preface

This article focuses on the tutorial used by SpringBoot's Transaction of Things.

SpringBoot Transaction

Note: If you want to get the project directly, you can skip to the bottom and download the project code through the link.

Transaction

Transaction management

In Spring, transactions are implemented in two ways, programmatic and declarative.

  • Programmatic transaction management: Programmatic transaction management uses TransactionTemplate or directly uses the underlying PlatformTransactionManager.For programmatic transaction management, spring recommends TransactionTemplate.
  • Declarative transaction management: built on AOP.The essence is to intercept before and after the method, create or join a transaction before the target method starts, and commit or roll back the transaction based on the execution after the target method is executed.
    Declarative transaction management does not require intrusion codes. Transaction operations can be performed through @Transactional ity, which is faster and easier and is recommended.

Transaction commit mode

By default, the database is in automatic commit mode.Each statement is in a separate transaction. At the end of execution, the transaction is implicitly committed if it succeeds and rolled back if it fails.
For normal transaction management, a set of related operations is in a transaction, so the automatic commit mode of the database must be turned off.However, we don't need to worry about this. Spring will set the autocommit feature of the underlying connection to false.That is, when using spring for things management, spring will set whether automatic commit to false, which is equivalent to connection.setAutoCommit(false) in JDBC; commit after execution, connection.commit();.

Transaction isolation level

Isolation level refers to the degree of isolation between several concurrent transactions.The TransactionDefinition interface defines five constants representing isolation levels:

  • TransactionDefinition.ISOLATION_DEFAULT: This is the default value indicating the default isolation level using the underlying database.For most databases, this value is usually TransactionDefinition.ISOLATION_READ_COMMITTED.
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED: This isolation level indicates that one transaction can read data modified by another transaction but not yet committed.This isolation level is rarely used because it does not prevent dirty reads, unrepeatable reads, and magic reads.PostgreSQL, for example, does not actually have this level.
  • TransactionDefinition.ISOLATION_READ_COMMITTED: This isolation level indicates that one transaction can only read data already committed by another transaction.This level prevents dirty reading, which is recommended in most cases.
  • TransactionDefinition.ISOLATION_REPEATABLE_READ: This isolation level indicates that a transaction can repeat a query multiple times throughout the process and that the records returned are the same each time.This level prevents dirty and non-repeatable reading.
  • TransactionDefinition.ISOLATION_SERIALIZABLE: All transactions execute one by one, so that there is no possibility of interference between transactions, that is, this level prevents dirty, non-repeatable, and magic reads.However, this will seriously affect program performance.Normally this level is not used.

Transaction Propagation Behavior

The propagating behavior of a transaction means that if a transaction context already exists before the current transaction begins, there are several options to specify the execution behavior of a transactional method.The following constants representing propagation behavior are included in the TransactionDefinition definition:

  • TransactionDefinition.PROPAGATION_REQUIRED: Join a transaction if it currently exists; create a new transaction if it does not.This is the default value.
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW: Creates a new transaction and suspends it if it currently exists.
  • TransactionDefinition.PROPAGATION_SUPPORTS: Join a transaction if it currently exists, and continue running non-transactionally if there is no transaction.
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED: Runs non-transactionally and suspends the current transaction if one exists.
  • TransactionDefinition.PROPAGATION_NEVER: Runs non-transactionally and throws an exception if a transaction currently exists.
  • TransactionDefinition.PROPAGATION_MANDATORY: Join the transaction if it currently exists; throw an exception if it does not.
  • TransactionDefinition.PROPAGATION_NESTED: Creates a transaction to run as a nested transaction of the current transaction if a transaction exists; if there is no transaction, the value is equivalent to TransactionDefinition.PROPAGATION_REQUIRED.

Transaction rollback rules

The recommended way to instruct the spring transaction manager to roll back a transaction is to throw an exception within the context of the current transaction.The spring transaction manager catches any unhandled exceptions and then decides whether to roll back the transaction that threw the exception based on the rules.
By default, spring rolls back the transaction only if it throws an exception that is a run-time unchecked exception, that is, if it throws an exception that is a subclass of RuntimeException (Errors also causes a transaction rollback), while throwing a checked exception does not cause a transaction rollback.
You can explicitly configure to roll back transactions when those exceptions are thrown, including checked exceptions.You can also explicitly define those exceptions that do not roll back transactions when thrown.

Transaction Common Configurations

  • ReadOnly: This property sets whether the current transaction is read-only, true means read-only, false means read-write, and the default value is false.For example: @Transactional(readOnly=true);
  • RollbackFor: This property sets the array of exception classes that need to be rolled back, and transaction rollback occurs when an exception in the specified exception array is thrown in the method.For example, specify a single exception class: @Transactional(rollbackFor=RuntimeException.class) specify multiple exception classes: @Transactional(rollbackFor={RuntimeException.class, Exception.class});
  • RollbackForClassName: This property sets the array of exception class names that need to be rolled back, and transaction rollback occurs when an exception in the specified exception name array is thrown in the method.For example, specify a single exception class name @Transactional(rollbackForClassName="RuntimeException") specify multiple exception class names: @Transactional(rollbackForClassName={"RuntimeException", "Exception"}.
  • NoRollbackFor: This property is used to set an array of exception classes that do not need to be rolled back, and no transaction rollback occurs when an exception in the specified exception array is thrown in the method.For example, specify a single exception class: @Transactional(noRollbackFor=RuntimeException.class) specify multiple exception classes: @Transactional(noRollbackFor={RuntimeException.class, Exception.class}).
  • NoRollbackForClassName: This property is used to set an array of exception class names that do not need to be rolled back, and no transaction rollback occurs when an exception in the specified exception name array is thrown in the method.For example, specify a single exception class name: @Transactional(noRollbackForClassName="RuntimeException") specify multiple exception class names: @Transactional(noRollbackForClassName={"RuntimeException",""Exception"}.
  • Propagation: This property sets the propagation behavior of the transaction.For example: @Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true).
  • Isolation: This property is used to set the transaction isolation level of the underlying database, which is used to handle multitransaction concurrency cases. The default isolation level of the database is usually used and does not need to be set at all.
  • Timeout: This property sets the number of seconds a transaction will timeout, and the default value is -1 to indicate that it will never timeout.

Things to Note

  1. To decide whether to use something based on actual needs, it is best to think about it before coding, or it will be difficult to maintain it later.
  2. If you are using things, be sure to test them, because in many cases things are considered valid, but may not actually work!
  3. Of the public methods where the use of transaction @Transactionalis to be reclassified, it is important to note that the @Transactionalannotation is used on the protected, private methods and it will not error (IDEA will prompt), but the transaction is invalid.
  4. Thing@Transactionalis not going to work on the submethods within this method!That is, you declare the thing @Transactional in public method A, but there are sub-methods B and C in method A, where method B performs data manipulation, but the exception is handled by B itself, so the thing will not work!Conversely, if method B declares @Transactional, but public method A does not declare anything, it will not work!If you want things to take effect, you need to give the transaction control of the submethod to the calling method. In the submethod, use the rollbackFor annotation to specify the exception that needs to be rolled back or throw the exception to the calling method for processing.A sentence is when using something the sub-method is best to throw an exception!
  5. When something @Transactional is controlled by spring, it rolls back when an exception is thrown.It is not valid if the process is captured and handled by the catch itself. Manual rollback is possible if the process is to take effect or an exception is thrown inside the catch, such as throw new RuntimeException();.

Development Preparation

Environmental requirements

JDK: 1.8

SpringBoot: 1.5.17.RELEASE

First, Maven's dependencies:

The pom.xml file is as follows:

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
  </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.17.RELEASE</version>
        <relativePath /> 
    </parent>
  <dependencies>
        <!-- Spring Boot Web Dependency Core -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
            <!-- Spring Boot Test rely on -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
      <dependency>
          <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
          <version>1.2.0</version>
      </dependency>
      <!-- MySQL Connection Driven Dependency -->
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.44</version>
      </dependency>
      <!-- Druid Data Connection Pool Dependency -->
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid</artifactId>
          <version>1.1.8</version>
      </dependency>
  </dependencies>

Configuration of the application.properties file:

banner.charset=UTF-8
server.tomcat.uri-encoding=UTF-8
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.messages.encoding=UTF-8
spring.application.name=springboot-transactional
server.port=8182

spring.datasource.url=jdbc:mysql://localhost:3306/springBoot?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.filters=stat,wall,log4j
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

logging.level.com.pancm.dao=debug

Coding

When SpringBoot uses Transactional things, add the @EnableTransactionManagement annotation to the main method to develop the declaration of things, and the @Transactional (spring) annotation to the public method of the service layer used.

Example 1

So first let's look at how to use the @Transactional annotation, as long as you add it above the public method you need to add.However, this use requires that you throw exceptions, which spring controls.

Code example:

    @Transactional
    public boolean test1(User user) throws Exception {
        long id = user.getId();
        System.out.println("Queried data 1:" + udao.findById(id));
        // Add two times and there will be a primary key ID conflict to see if the data can be rolled back
        udao.insert(user);
        System.out.println("Queried Data 2:" + udao.findById(id));
        udao.insert(user);
        return false;
    }

Example 2

If we want to handle exceptions ourselves when using @Transactional, we can roll things back manually.Add TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() to the catch; the method is rolled back manually.However, it is important to note that an exception needs to be rolled back manually the first time it occurs, that is, before the exception is thrown!

Code example:

    @Transactional
    public boolean test2(User user) {

        long id = user.getId();
        try {
            System.out.println("Queried data 1:" + udao.findById(id));
            // Add two times and there will be a primary key ID conflict to see if the data can be rolled back
            udao.insert(user);
            System.out.println("Queried Data 2:" + udao.findById(id));
            udao.insert(user);
        } catch (Exception e) {
            System.out.println("exception occurred,Do manual rollback!");
            // Roll back things manually
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            e.printStackTrace();
        }
        return false;
    }

Example 3

If we invoke other submethods to operate on the database when using transaction @Transactional, but we want to make it work, we can use the rollbackFor annotation or throw the exception of this submethod into the called method to handle it, but here it is important to note that the submethod must also be a public method!

Code example:

@Transactional
    public boolean test3(User user) {

        /*
         * Submethod has an exception rollback
         */
        try {
            System.out.println("Queried data 1:" + udao.findById(user.getId()));
            deal1(user);
            deal2(user);
            deal3(user);
        } catch (Exception e) {
            System.out.println("exception occurred,Do manual rollback!");
            // Roll back things manually
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            e.printStackTrace();
        } 
        return false;

    }

    public void deal1(User user) throws SQLException {
        udao.insert(user);
        System.out.println("Queried Data 2:" + udao.findById(user.getId()));
    }

    public void deal2(User user)  throws SQLException{
        if(user.getAge()<20){
            //SQL Exception
            udao.insert(user);
        }else{
            user.setAge(21);
            udao.update(user);
            System.out.println("Queried data 3:" + udao.findById(user.getId()));
        }
    }


    @Transactional(rollbackFor = SQLException.class)
    public void deal3(User user)  {
        if(user.getAge()>20){
            //SQL Exception
            udao.insert(user);
        }

    }

Example 4

If we don't want to use the @Transactional annotation for things, do things by ourselves (programmatic things management), control a piece of code that works, but don't want to write so much code on our own, we can use the DataSourceTransactionManager and TransactionDefinition classes in springboot together to achieve the manual control of things.Rollback.However, when using it, you need to be aware that when you roll back, make sure that you open something but do not submit it it. If you do not open something or submit it it, rollback will occur abnormally in the catch!

Code example:

    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;
    @Autowired
    private TransactionDefinition transactionDefinition;

    public boolean test4(User user) {
        /*
         * Manually control things
         */
        TransactionStatus transactionStatus=null;
        boolean isCommit = false;
        try {
            transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
            System.out.println("Queried data 1:" + udao.findById(user.getId()));
            // Make additions/modifications
            udao.insert(user);
            System.out.println("Queried Data 2:" + udao.findById(user.getId()));
            if(user.getAge()<20) {
                user.setAge(user.getAge()+2);
                udao.update(user);
                System.out.println("Queried data 3:" + udao.findById(user.getId()));
            }else {
                throw new Exception("Simulate an exception!");
            }
            //Manual Submission
            dataSourceTransactionManager.commit(transactionStatus);
            isCommit= true;
            System.out.println("Manual Submission Succeeded!");
            throw new Exception("Simulate the second exception!");

        } catch (Exception e) {
            //Rollback if not submitted
            if(!isCommit){
                System.out.println("exception occurred,Do manual rollback!");
                //Roll back things manually
                dataSourceTransactionManager.rollback(transactionStatus);
            }
            e.printStackTrace();
        }
        return false;
    }

These examples are common and can basically satisfy our daily use of things. There is also a way to control things in spring, which is to set breakpoints and roll back.However, this method has not been personally verified and its reliability needs to be confirmed.
Use it as follows:

    Object savePoint =null;
    try{
    //Set rollback point
    savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
    }catch(Exception e){
        //An exception occurred and rolled back to savePoint.
     TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
    }

Once the above usage examples are complete, let's move on to a few main classes.

First, entity classes:

Entity Class

It's also the omnipotent user table

    public class User {
        
         private Long id;
    
         private String name;
         
         private Integer age;
         
        //getter and setter omitted
        
    }

Controller Control Layer

Then there's the control layer. I've made a final inquiry on the control layer to verify that things are working successfully!

The control layer code is as follows:

    @RestController
    @RequestMapping(value = "/api/user")
    public class UserRestController {
    
        @Autowired
        private UserService userService;
        
        @Autowired
        private UserDao userDao;
        
    
        @PostMapping("/test1")
        public boolean test1(@RequestBody User user) {
            System.out.println("Request parameters:" + user);
            try {
                userService.test1(user);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("Data last queried:" + userDao.findById(user.getId()));
            return true;
        }
        
        @PostMapping("/test2")
        public boolean test2(@RequestBody User user) {  
            System.out.println("Request parameters:" + user);
            userService.test2(user);
            System.out.println("Data last queried:" + userDao.findById(user.getId()));
            return true;
        }
                
        @PostMapping("/test3")
        public boolean test3(@RequestBody User user) {  
            System.out.println("Request parameters:" + user);
            userService.test3(user);
            System.out.println("Data last queried:" + userDao.findById(user.getId()));
            return true;
        }
        
        @PostMapping("/test4")
        public boolean test4(@RequestBody User user) {  
            System.out.println("Request parameters:" + user);
            userService.test4(user);
            System.out.println("Data last queried:" + userDao.findById(user.getId()));
            return true;
        }
    }

App Entry

Similar to normal SpringBoot projects, just add the @EnableTransactionManagement comment!

The code is as follows:

    @EnableTransactionManagement
    @SpringBootApplication
    public class TransactionalApp
    {
            
        public static void main( String[] args )
        {
            SpringApplication.run(TransactionalApp.class, args);
            System.out.println("Transactional The program is running...");
        
        }
    }

functional testing

After we start the program, we will do some of the above sample tests, which correspond to the above usage examples, some of which need to be tested on more than two sides to verify whether something works!Here we use Postman Test!

Test Example 1

Two tests, the first time without the @Transactional annotation, the second time!

First test:

Comment out the @Transactional comment!
Use for POST requests

http://localhost:8182/api/user/test1

The Body parameter is:

{"id":1,"name":"xuwujing","age":18}

Console Printed Data:

 Request parameters: User [id=1, name=xuwujing, age=18]
 Queried data 1:null
 Queried data 2:User [id=1, name=xuwujing, age=18]
 Duplicate entry '1' for key 'PRIMARY'
 Last data queried: User [id=1, name=xuwujing, age=18]

Second test:

Uncomment @Transactional!

Use for POST requests

http://localhost:8182/api/user/test1

The Body parameter is:

{"id":1,"name":"xuwujing","age":18}

Console Printed Data:

 Request parameters: User [id=1, name=xuwujing, age=18]
 Queried data 1:null
 Queried data 2:User [id=1, name=xuwujing, age=18]
 Duplicate entry '1' for key 'PRIMARY'
 Last queried data: null

Note: Before the second test, the first test written to the database with id 1 was deleted!

Exception data was written in the first test because the @Transactional annotation was not added, but the @Transactional annotation was added in the second test. It was found that even though the data was written, after the exception occurred, the data was rolled back and not written!
You can see from the above test cases that one of the things in the test cases is working!

Test Example 2

Since using the code in Example 2 is almost the same as using Example 1, the difference is that exceptions are controlled by ourselves!

Use for POST requests

http://localhost:8182/api/user/test2

The Body parameter is:

{"id":1,"name":"xuwujing","age":18}

Console Printed Data:

 Request parameters: User [id=1, name=xuwujing, age=18]
 Queried data 1:null
 Queried data 2:User [id=1, name=xuwujing, age=18]
 An exception occurred, rollback manually!
 Duplicate entry '1' for key 'PRIMARY'
 Last queried data: null

You can see that things are working!

Test Example Three

Because of the submethod calls in Example 3, here we do two tests to test against different request conditions!

First test:

Use for POST requests

http://localhost:8182/api/user/test3

The Body parameter is:

{"id":1,"name":"xuwujing","age":18}

Console Printed Data:

 Request parameters: User [id=1, name=xuwujing, age=18]
 Queried data 1:null
 Queried data 2:User [id=1, name=xuwujing, age=18]
 An exception occurred, rollback manually!
 Duplicate entry '1' for key 'PRIMARY'
 Last queried data: null

Second test:

Use for POST requests

http://localhost:8182/api/user/test3

The Body parameter is:

{"id":1,"name":"xuwujing","age":21}

Console Printed Data:

 Request parameters: User [id=1, name=xuwujing, age=21]
 Queried data 1:null
 Queried data 2:User [id=1, name=xuwujing, age=21]
 Query data 3:User [id=1, name=xuwujing2, age=21]
 An exception occurred, rollback manually!
 Duplicate entry '1' for key 'PRIMARY'
 Last queried data: null

Based on the two tests above, you can conclude that using the rollbackFor comment or throwing an exception of this submethod to be handled by the called method will make things work!

Test Example 4

Since you used Example 4 to control things manually, here we do two tests to test against different request conditions!

First test:

Use for POST requests

http://localhost:8182/api/user/test4

The Body parameter is:

{"id":1,"name":"xuwujing","age":18}

Console Printed Data:

  Request parameters: User [id=1, name=xuwujing, age=18]
  Queried data 1:null
  Queried data 2:User [id=1, name=xuwujing, age=18]
  Query data 3:User [id=1, name=xuwujing2, age=20]
  Manual submission was successful!
  Simulate the second exception!
  Last data queried: User [id=1, name=xuwujing, age=20]

Second test:

Delete data with database id 1 beforehand!

Use for POST requests

http://localhost:8182/api/user/test4

The Body parameter is:

{"id":1,"name":"xuwujing","age":21}

Console Printed Data:

 Request parameters: User [id=1, name=xuwujing, age=21]
 Queried data 1:null
 Queried data 2:User [id=1, name=xuwujing, age=21]
 An exception occurred, rollback manually!
 Simulate an exception!
 Last queried data: null

Based on the above two tests, we can conclude that using manual control of things is completely ok, as long as something is submitted, even if an exception occurs later, it will not affect the previous writing!If an exception occurs within the scope of the control, it can also be rolled back!

Test sample diagram:

Other

Reference resources:
https://www.cnblogs.com/yepei/p/4716112.html

Project Address

Project project address for the SpringBoot transaction Transaction:
https://github.com/xuwujing/springBoot-study/tree/master/springboot-transactional

Address of the entire SpringBoot collection:
https://github.com/xuwujing/springBoot-study

Forty-five original articles were published. 209 were praised. 360,000 visits were received+
Private letter follow

Tags: Spring SpringBoot Database encoding

Posted on Thu, 13 Feb 2020 22:34:19 -0500 by Jeremias