Several case s where transactions for the SpringBoot series of tutorials do not work

Several case s where transactions for the SpringBoot series of tutorials do not work

Previous posts on declarative transactions @Transactional use posture, knowing only that the correct posture may not be enough, and what scenarios do not work to avoid pit mining.This article will focus on a few case s where transactions do not take effect

<!-- 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. Invalid case

Tutorial on using declarative transactions Declarative Transactional Transactional for the SpringBoot series of tutorials Some ways in which transactions do not take effect are also mentioned, such as the declarative transaction annotation @Transactional which is mainly implemented with a proxy, combined with AOP's knowledge points, at least on a private method, where class internal calls will not take effect, are detailed below.

1. Database

Transactions take effect only if your data source supports transactions, such as mysql's MyISAM engine, which does not support transactions, while Innodb supports transactions

The following case s are all based on the mysql + Innodb engine

For the following demonstration case s, we'll prepare some data as follows

@Service
public class NotEffectDemo {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void init() {
        String sql = "replace into money (id, name, money) values" + " (520, 'Initialization', 200)," + "(530, 'Initialization', 200)," +
                "(540, 'Initialization', 200)," + "(550, 'Initialization', 200)";
        jdbcTemplate.execute(sql);
    }
}

2. Class internal access

Simply put, it means that instead of directly accessing annotated method B, it accesses B through class generic method A, which then accesses B from A

Here is a simple case

/**
 * Not directly invoked, not valid
 *
 * @param id
 * @return
 * @throws Exception
 */
@Transactional(rollbackFor = Exception.class)
public boolean testCompileException2(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("after update name", id);
        if (this.update(id)) {
            return true;
        }
    }

    throw new Exception("Parameter Exception");
}

public boolean testCall(int id) throws Exception {
    return testCompileException2(id);
}

The above two methods call the testCompleException method directly, and the transaction operates normally; indirect access by calling testCall does not take effect

The test case is as follows:

@Component
public class NotEffectSample {
    @Autowired
    private NotEffectDemo notEffectDemo;

    public void testNotEffect() {
        testCall(530, (id) -> notEffectDemo.testCall(530));
    }

    private void testCall(int id, CallFunc<Integer, Boolean> func) {
        System.out.println("============ Transaction is not valid case start ========== ");
        notEffectDemo.query("transaction before", id);
        try {
            // Transactions can work
            func.apply(id);
        } catch (Exception e) {
        }
        notEffectDemo.query("transaction end", id);
        System.out.println("============ Transaction is not valid case end ========== \n");
    }

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

The output is as follows:

============ Transaction is not valid case start ==========
transaction before >>>> {id=530, name=Initialization, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
after update name >>>> {id=530, name=To update, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
transaction end >>>> {id=530, name=To update, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
============ Transaction is not valid case end ==========

From the output above, you can see that the transaction was not rolled back, mainly because it was called inside the class and was not accessed through a proxy

3. Private methods

On private methods, adding the @Transactional annotation will not work, private methods cannot be accessed externally, so they can only be accessed internally. The case above does not work, and of course it will not work either

/**
 * Comments on private methods, not valid
 *
 * @param id
 * @return
 * @throws Exception
 */
@Transactional
private boolean testSpecialException(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("after update name", id);
        if (this.update(id)) {
            return true;
        }
    }

    throw new Exception("Parameter Exception");
}

When used directly, this scenario is not very easy because IDEA has reminders in the following text: Methods annotated with'@Transactional'must be overridable

4. Exception mismatch

The @Transactional annotation handles runtime exceptions by default, that is, transaction rollbacks are triggered only when a runtime exception is thrown, otherwise they are not

/**
 * Non-running exception, no exception thrown by rollbackFor specified, not valid
 *
 * @param id
 * @return
 * @throws Exception
 */
@Transactional
public boolean testCompleException(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("after update name", id);
        if (this.update(id)) {
            return true;
        }
    }

    throw new Exception("Parameter Exception");
}

The test case is as follows

public void testNotEffect() {
    testCall(520, (id) -> notEffectDemo.testCompleException(520));
}

The output is as follows, the transaction is not rolled back (if you need to solve this problem, set the rollbackFor property of @Transactional ly)

============ Transaction is not valid case start ==========
transaction before >>>> {id=520, name=Initialization, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
after update name >>>> {id=520, name=To update, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
transaction end >>>> {id=520, name=To update, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
============ Transaction is not valid case end ==========

5. Multithreading

This may not be a common scenario where, within the method of marking transactions, another child thread executes a db operation and the transaction does not take effect

The following two different positions are given, one is the child thread throwing an exception, the main thread ok; the other is the child thread ok, the main thread throwing an exception

a. case1

/**
 * The child thread threw an exception and the main thread could not catch it, causing the transaction to not take effect
 *
 * @param id
 * @return
 */
@Transactional(rollbackFor = Exception.class)
public boolean testMultThread(int id) throws InterruptedException {
    new Thread(new Runnable() {
        @Override
        public void run() {
            updateName(id);
            query("after update name", id);
        }
    }).start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            boolean ans = update(id);
            query("after update id", id);
            if (!ans) {
                throw new RuntimeException("failed to update ans");
            }
        }
    }).start();

    Thread.sleep(1000);
    System.out.println("------- Child Thread --------");

    return true;
}

It's understandable that this scenario doesn't work. Exceptions to child threads are not caught by external threads, and calls to testMultThread do not throw exceptions, so transaction rollbacks are not triggered

public void testNotEffect() {
    testCall(540, (id) -> notEffectDemo.testMultThread(540));
}

The output is as follows

============ Transaction is not valid case start ==========
transaction before >>>> {id=540, name=Initialization, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
after update name >>>> {id=540, name=To update, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
Exception in thread "Thread-3" java.lang.RuntimeException: failed to update ans
	at com.git.hui.boot.jdbc.demo.NotEffectDemo$2.run(NotEffectDemo.java:112)
	at java.lang.Thread.run(Thread.java:748)
after update id >>>> {id=540, name=To update, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
------- Child Thread --------
transaction end >>>> {id=540, name=To update, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
============ Transaction is not valid case end ==========

b. case2

/**
 * The child thread threw an exception and the main thread could not catch it, causing the transaction to not take effect
 *
 * @param id
 * @return
 */
@Transactional(rollbackFor = Exception.class)
public boolean testMultThread2(int id) throws InterruptedException {
    new Thread(new Runnable() {
        @Override
        public void run() {
            updateName(id);
            query("after update name", id);
        }
    }).start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            boolean ans = update(id);
            query("after update id", id);
        }
    }).start();

    Thread.sleep(1000);
    System.out.println("------- Child Thread --------");

    update(id);
    query("after outer update id", id);

    throw new RuntimeException("failed to update ans");
}

This one above looks like it's okay, threads thrown, transactions rolled back, unfortunately the modifications of the two sub-threads won't be rolled back

Test Code

public void testNotEffect() {
    testCall(550, (id) -> notEffectDemo.testMultThread2(550));
}

As you can also see from the output below, modifications to the child threads are not within the same transaction and will not be rolled back

============ Transaction is not valid case start ==========
transaction before >>>> {id=550, name=Initialization, money=200, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:38.0}
after update name >>>> {id=550, name=To update, money=200, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
after update id >>>> {id=550, name=To update, money=210, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
------- Child Thread --------
after outer update id >>>> {id=550, name=To update, money=220, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:41.0}
transaction end >>>> {id=550, name=To update, money=210, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
============ Transaction is not valid case end ==========

6. Propagation Properties

In the previous blog on communication properties, several of them are not executed without transaction, so additional attention is needed. Details can be found in the blog Transaction Delivery Properties for the 200202-SpringBoot series of tutorials

7. Summary

The following is a summary of several case s where the @Transactional annotation transaction is not valid

  • Database does not support transactions
  • Notes were liberated on private methods
  • Class internal call
  • No exceptions were caught
  • Multithreaded Scenario
  • Propagation Property Settings Problem

III. 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

Tags: Programming SpringBoot Spring MySQL Database

Posted on Tue, 04 Feb 2020 17:03:37 -0800 by Rupuz