Transaction Delivery Properties for SpringBoot Series Tutorials

Keywords: Programming Spring SpringBoot MySQL Database

Transaction Delivery Properties for the 200202-SpringBoot series of tutorials

For mysql, there may be several main points of knowledge about transactions at the isolation level; in the Spring system, there is also a transfer property of knowledge point transactions that is equally important when using transactions. This article will focus on the scenarios in which the transfer property is used in 7.

<!-- more -->

I. Configuration

In this case, declarative transactions are used. First, we create a SpringBoot project, version 2.2.1.RELEASE, using mysql as the target database, the storage engine chooses Innodb, and the transaction isolation level is RR

1. Project Configuration

In the project pom.xml file, plus spring-boot-starter-jdbc, a DataSourceTransactionManager bean is injected, providing transaction support

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

2. Database Configuration

Go to the spring configuration file application.properties to set up db-related information

## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=

3. Database

Create a new simple table structure for testing

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT 'User name',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT 'money',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update Time',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

II. Instructions for use

0. Preparation

Before you get started, you have to prepare some basic data

@Component
public class PropagationDemo {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void init() {
        String sql = "replace into money (id, name, money) values (420, 'Initialization', 200)," + "(430, 'Initialization', 200)," +
                "(440, 'Initialization', 200)," + "(450, 'Initialization', 200)," + "(460, 'Initialization', 200)," + "(470, 'Initialization', 200)," +
                "(480, 'Initialization', 200)," + "(490, 'Initialization', 200)";
        jdbcTemplate.execute(sql);
    }
}

Next, to test the use of transactions, we need to create an additional test class, with subsequent test case s placed in the class PropagationSample; to make the output more friendly, an encapsulated call method is provided

@Component
public class PropagationSample {
    @Autowired
    private PropagationDemo propagationDemo;
    
    private void call(String tag, int id, CallFunc<Integer> func) {
        System.out.println("============ " + tag + " start ========== ");
        propagationDemo.query(tag, id);
        try {
            func.apply(id);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        propagationDemo.query(tag, id);
        System.out.println("============ " + tag + " end ========== \n");
    }


    @FunctionalInterface
    public interface CallFunc<T> {
        void apply(T t) throws Exception;
    }
}

1. REQUIRED

It is also the default delivery property, which is characterized by

  • Run in the current transaction if one exists
  • Start a new transaction if there are no transactions

Simple to use, do not set the propagation property of the @Transactional annotation, or set it to REQUIRED

/**
 * If a transaction exists, the current transaction is supported.Start a new transaction if there are no transactions
 *
 * @param id
 */
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void required(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("required: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }

    throw new Exception("rollback!!!");
}

Above is a basic usage position

private void testRequired() {
    int id = 420;
    call("Required Transaction Run", id, propagationDemo::required);
}

The output is as follows

================= Required transaction run start ========== 
Required transaction run >> {id=420, name=initialization, money=200, is_deleted=false, create_at=2020-02-15:23:26.0, update_at=2020-02 15:23:26.0}
Required: after updateMoney name >> {id=420, name=update, money=200, is_deleted=false, create_at=2020-02 15:23:26.0, update_at=2020-02 15:23:46.0}
Transaction rollback!!!
Required transaction run >> {id=420, name=initialization, money=200, is_deleted=false, create_at=2020-02-15:23:26.0, update_at=2020-02 15:23:26.0}
================= Required transaction run end ========== 

2. SUPPORTS

It is characterized by execution within a transaction; otherwise, it is not transactional, i.e.

  • Supports the current transaction if one exists
  • Non-transactional execution if no transaction exists

Use the same posture as the front

@Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
public void support(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("support: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }

    throw new Exception("rollback!!!");
}

This pass-through property is special, so our test case requires two, one transactional call and one non-transactional call

When testing the transaction call, we create a new bean: PropagationDemo2, the following support method supports the transaction

@Component
public class PropagationDemo2 {
    @Autowired
    private PropagationDemo propagationDemo;

    @Transactional(rollbackFor = Exception.class)
    public void support(int id) throws Exception {
        // Transaction Run
        propagationDemo.support(id);
    }
}

For non-transactional calls, they are called directly in the test class (note the call method below, which calls the support method in two different bean s)

private void testSupport() {
    int id = 430;
    // Non-transactional, exception does not roll back
    call("support Run without transaction", id, propagationDemo::support);

    // Transaction Run
    id = 440;
    call("support Transaction Run", id, propagationDemo2::support);
}

The output is as follows:

================ support No transaction run start ========= 
Supports Transactionless Run >> {id=430, name=initialization, money=200, is_deleted=false, create_at=2020-02 15:23:26.0, update_at=2020-02 15:23:26.0}
Support: after updateMoney name >> {id=430, name=update, money=200, is_deleted=false, create_at=2020-02-15:23:26.0, update_at=2020-02 15:23:46.0}
Transaction rollback!!!
Supports Transactionless Run >> {id=430, name=update, money=210, is_deleted=false, create_at=2020-02 15:23:26.0, update_at=2020-02 15:23:46.0}
================ support No transaction run end ========= 

================ support transaction run start ========== 
SupportTransaction Run >>> {id=440, name=initialization, money=200, is_deleted=false, create_at=2020-02 15:23:26.0, update_at=2020-02 15:23:26.0}
Support: after updateMoney name >> {id=440, name=update, money=200, is_deleted=false, create_at=2020-02-15:23:26.0, update_at=2020-02 15:23:46.0}
Transaction rollback!!!
SupportTransaction Run >>> {id=440, name=initialization, money=200, is_deleted=false, create_at=2020-02 15:23:26.0, update_at=2020-02 15:23:26.0}
================ support transaction run end ========== 

From the above output, you can also see that when a non-transaction is executed, it is not rolled back; when a transaction is executed, it is rolled back

3. MANDATORY

Need to execute within a normal transaction or throw an exception

Use position as follows

@Transactional(propagation = Propagation.MANDATORY, rollbackFor = Exception.class)
public void mandatory(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("mandatory: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }

    throw new Exception("rollback!!!");
}

The characteristic of this propagation property is that this method must run in an existing transaction, so our test case is simpler, what happens when it no longer runs in a transaction?

private void testMandatory() {
    int id = 450;
    // Non-transactional, throws an exception, which must be executed within a transaction
    call("mandatory Non-transactional Run", id, propagationDemo::mandatory);
}

Output Results

============ mandatory Non-transactional Run start ========== 
mandatory Non-transactional Run >>>> {id=450, name=Initialization, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
No existing transaction found for transaction marked with propagation 'mandatory'
mandatory Non-transactional Run >>>> {id=450, name=Initialization, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ mandatory Non-transactional Run end ========== 

From the output above, you can see that the exception is thrown directly, and the logic within the method is not executed.

4. NOT_SUPPORT

This is interesting because the method marked by it always executes non-transactionally and suspends if there is an active transaction

(It's hard to imagine what scene needs this propagating property)

A simple case to use is as follows:

@Transactional(propagation = Propagation.NOT_SUPPORTED, rollbackFor = Exception.class)
public void notSupport(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("notSupport: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }
    throw new Exception("RollBACK!");
}

Next, we need to think about our test case. First, it needs to be invoked in a transaction. External things fail and roll back without affecting the execution result of the above method.

We added the following test case in PropagationDemo2

@Transactional(rollbackFor = Exception.class)
public void notSupport(int id) throws Exception {
    // Suspend the current transaction and run non-transactionally
    try {
        propagationDemo.notSupport(id);
    } catch (Exception e) {
    }

    propagationDemo.query("notSupportCall: ", id);
    propagationDemo.updateName(id, "External Update");
    propagationDemo.query("notSupportCall: ", id);
    throw new Exception("RollBACK");
}

The output is as follows

============ notSupport start ========== 
NotSupport >>> {id=460, name=initialization, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02 15:23:26.0}
NotSupport: after updateMoney name >> {id=460, name=update, money=200, is_deleted=false, create_at=2020-02-15:23:26.0, update_at=2020-02 15:23:46.0}
NotSupportCall: >>> {id=460, name=update, money=210, is_deleted=false, create_at=2020-02 15:23:26.0, update_at=2020-02 15:23:46.0}
NotSupportCall: >>> {id=460, name=external update, money=210, is_deleted=false, create_at=2020-02-15:23:26.0, update_at=2020-02 15:23:46.0}
RollBACK
 NotSupport >>> {id=460, name=update, money=210, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02 15:23:46.0}
============ notSupport end ========== 

You can see from the output above

  • NOT_SUPPORT tagged method, which is run non-transactionally (modifications are not rolled back because exceptions are thrown)
  • External transaction rollback without affecting its modifications

5. NEVER

Always executes non-transactionally and throws an exception if there is an active transaction.

Use position as follows

/**
 * Always executes non-transactionally and throws an exception if there is an active transaction.
 *
 * @param id
 * @throws Exception
 */
@Transactional(propagation = Propagation.NEVER, rollbackFor = Exception.class)
public void never(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("notSupport: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }
}

Our test is simpler, if it runs in a transaction, will it throw an exception

In PropagationDemo2, add a transaction call method

@Transactional(rollbackFor = Exception.class)
public void never(int id) throws Exception {
    propagationDemo.never(id);
}

Test Code

private void testNever() {
    int id = 470;
    call("never Non-transactional", id, propagationDemo2::never);
}

Output Results

============ never Non-transactional start ========== 
never Non-transactional >>>> {id=470, name=Initialization, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
Existing transaction found for transaction marked with propagation 'never'
never Non-transactional >>>> {id=470, name=Initialization, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ never Non-transactional end ==========

An exception was thrown directly without executing the business logic within the method

6. NESTED

Its main features are as follows

  • If no transaction exists, start a transaction run
  • Run a nested transaction if one exists;

A concept of nested transactions was proposed above. What is a nested transaction?

  • A simple understanding: external transactions are rolled back, internal transactions are also rolled back; internal transactions are rolled back, external problems are not rolled back, external transactions are not rolled back

Next, design two test cases, one for internal transaction rollback and one for external transaction rollback

a. case1 internal transaction rollback

@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void nested(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("nested: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }

    throw new Exception("rollback!!!");
}

In the PropagationDemo2 bean, an external transaction is added to catch the exception of the above method, so the external execution is normal

@Transactional(rollbackFor = Exception.class)
public void nested(int id) throws Exception {
    propagationDemo.updateName(id, "External Transaction Modification");
    propagationDemo.query("nestedCall: ", id);
    try {
        propagationDemo.nested(id);
    } catch (Exception e) {
    }
}

Test Code

private void testNested() {
    int id = 480;
    call("nested affair", id, propagationDemo2::nested);
}

The output is as follows

============ nested affair start ========== 
nested affair >>>> {id=480, name=Initialization, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
nestedCall:  >>>> {id=480, name=External Transaction Modification, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
nested: after updateMoney name >>>> {id=480, name=To update, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
nested affair >>>> {id=480, name=External Transaction Modification, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
============ nested affair end ==========

Looking carefully at the above results, the results of the external transaction modifications are saved, and the internal transaction modifications are rolled back without affecting the final results

b. case2 external transaction rollback

@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void nested2(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("nested: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }
}

In PropagationDemo2, an external transaction is added, the internal transaction is normal, but the external transaction throws an exception and rolls back actively

@Transactional(rollbackFor = Exception.class)
public void nested2(int id) throws Exception {
    // Nested transactions, external rollback, synchronous rollback of internal transactions
    propagationDemo.updateName(id, "External Transaction Modification");
    propagationDemo.query("nestedCall: ", id);
    propagationDemo.nested2(id);
    throw new Exception("rollback");
}

Test Code

private void testNested() {
    int id = 490;
    call("nested Transaction 2", id, propagationDemo2::nested2);
}

The output is as follows

================ nested transaction 2 start=========== 
nested transaction 2 >>> {id=490, name=initialization, money=200, is_deleted=false, create_at=2020-02-15:23:26.0, update_at=2020-02 15:23:26.0}
NestedCall: >>> {id=490, name=external transaction modification, money=200, is_deleted=false, create_at=2020-02 15:23:26.0, update_at=2020-02 15:23:46.0}
Nested: after updateMoney name >> {id=490, name=update, money=200, is_deleted=false, create_at=2020-02-15:23:26.0, update_at=2020-02 15:23:46.0}
rollback
 nested transaction 2 >>> {id=490, name=initialization, money=200, is_deleted=false, create_at=2020-02-15:23:26.0, update_at=2020-02 15:23:26.0}
================ nested transaction 2 end=========== 

Looking closely at the output above, case1, in particular, rolled back all the changes to the internal transaction

7. REQUIRES_NEW

This is a bit like NESTED above, but it's different

  • Create a new transaction to execute when an active transaction exists
  • Create a transaction execution when no active transaction exists, consistent with the REQUIRES effect

Be careful

REQUIRES_NEW has no relationship with NESTED, either rollback has no effect on the other

Test case is similar to the previous one, not much detail...

8. Summary

The propagation properties in 7 are described earlier, but here's a simple comparison and summary

affair Characteristic
REQUIRED By default, if a transaction exists, it supports the current transaction; if it does not exist, it opens a new transaction
SUPPORTS Supports the current transaction if one exists.Non-transactional execution if no transaction exists
MANDATORY Need to execute within a normal transaction or throw an exception
REQUIRES_NEW Start a new transaction whether it exists or not
NOT_SUPPORTED Executes non-transactionally regardless of whether the store does not exist, suspends the transaction when it exists
NEVER Executed non-transactionally, throws an exception if a transaction exists
NESTED If no transaction exists, a transaction is started; if a transaction exists, a nested transaction is run

II. Other

0.Series Blog & Source

Series Blog

Source code

1.A grey Blog

Unlike letters, the above are purely family statements. Due to limited personal abilities, there are unavoidable omissions and errors. If bug s are found or there are better suggestions, you are welcome to criticize and correct them with gratitude.

Below is a grey personal blog, which records all the blogs in study and work. Welcome to visit it

Posted by andy666 on Sun, 02 Feb 2020 20:01:59 -0800