@Transactional Rollback Problem (try catch, nested)

Posted by Brusca on Sun, 26 May 2019 18:35:54 +0200

The Spring transaction annotation @Transactional could have guaranteed atomicity, and the entire transaction could have guaranteed rollback if there were errors within the transaction, but adding try catch or transaction nesting could have caused the transaction rollback to fail.Test a wave.

Get ready

Build two tables to simulate two data operations

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `age` smallint(3) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

test

According to the principle of permutation and combination, we have four tests: 1, no try catch, no nesting; 2, try catch, no nesting; 3, no try catch, nesting; 4, all.

Simplest test

If we just @Transactional, can the transaction roll back normally?

    @GetMapping("/saveNormal0")
    @Transactional
    public void saveNormal0() throws Exception {
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:"+age);
        userService.save(user);
        throw new RuntimeException();
    }

If a RuntimeException error is reported within a transaction, the transaction can be rolled back.

    @GetMapping("/saveNormal0")
    @Transactional
    public void saveNormal0() throws Exception {
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:"+age);
        userService.save(user);
        throw new Exception();
    }

The transaction cannot be rolled back if an Exception error (not a RuntimeException error) is reported within the transaction.

    @GetMapping("/saveNormal0")
    @Transactional( rollbackFor = Exception.class)
    public void saveNormal0() throws Exception {
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:"+age);
        userService.save(user);
        throw new Exception();
    }

If it is an Exception error (not a RuntimeException), rollbackFor = Exception.class parameter can also be used to rollback.

Conclusion 1: For @Transactional ly, the rollback of RuntimeException errors can be guaranteed. If you want to guarantee the rollback of non-RuntimeException errors, you need to add the rollbackFor = Exception.class parameter.

try catch effect

After a variety of blogger tests, we found that try catch had no effect on the rollback itself, and the conclusion was the same.Try catch only has an effect on whether an exception can be perceived by @Transactional s.If the error is thrown to a point where the facets can be perceived, it can work.

    @GetMapping("/saveTryCatch")
    @Transactional( rollbackFor = Exception.class)
    public void saveTryCatch() throws Exception{
        try{
            int age = random.nextInt(100);
            User user = new User().setAge(age).setName("name:"+age);
            userService.save(user);
            throw new Exception();
        }catch (Exception e){
            throw e;
        }
    }

For example, the code above rolls back.

    @GetMapping("/saveTryCatch")
    @Transactional( rollbackFor = Exception.class)
    public void saveTryCatch() throws Exception{
        try{
            int age = random.nextInt(100);
            User user = new User().setAge(age).setName("name:"+age);
            userService.save(user);
            throw new Exception();
        }catch (Exception e){
        }
    }

However, if errors in the catch are not continued to be thrown on the net, the facets are not aware of the errors and cannot be processed, then the transaction cannot be rolled back.

Conclusion 2: try catch only affects whether the anomaly can be perceived by @Transactional s.If the error is thrown to a point where the facets can be perceived, it can work.

Transaction Nesting Impact

After experimentation, the first conclusion is still valid, that is, when rollbackFor = Exception.class is not added, both internal and external reports of RuntimeException will roll back; neither internal or external reports of non-RuntimeException errors will roll back.If rollbackFor = Exception.class is added, it will be rolled back regardless of internal or external errors.These codes are not given.Next, try the following two scenarios:

    @GetMapping("/out")
    @Transactional( rollbackFor = Exception.class)
    public void out() throws Exception{
        innerService.inner();
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:" + age);
        userService.save(user);
        throw new Exception();
    }
    @Transactional
    public void inner() throws Exception{
        Role role = new Role();
        role.setRoleName("roleName:"+new Random().nextInt(100));
        roleService.save(role);
//        throw new Exception();
    }

In case one, rollbackFor = Exception.class is added to the external transaction, while the internal transaction is not added. In case of separate errors inside and outside the test (in order to simplify the amount of code, only the external error code is given), it can be rolled back.Because, in any case, the error is thrown to the outside transaction for processing, and the outside transaction with rollbackFor = Exception.class has the ability to handle non-RuntimeException errors, it is possible to roll back the transaction normally.

Look at scenario two below. The transaction inside is rollbackFor = Exception.class, the transaction outside is not, and the error outside is made.

    @GetMapping("/out")
    @Transactional
    public void out() throws Exception{
        innerService.inner();
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:" + age);
        userService.save(user);
        throw new Exception();
    }
    
    @Transactional( rollbackFor = Exception.class)
    public void inner() throws Exception{
        Role role = new Role();
        role.setRoleName("roleName:"+new Random().nextInt(100));
        roleService.save(role);
    }

Transactions can not be rolled back, which is our question. The transactions inside clearly have a strong processing power. Why do we fail to roll back together with the outside? Don't worry, wait and talk about this.

Then try to make an error inside:

    @GetMapping("/out")
    @Transactional
    public void out() throws Exception{
        innerService.inner();
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:" + age);
        userService.save(user);
    }
     @Transactional( rollbackFor = Exception.class)
    public void inner() throws Exception{
        Role role = new Role();
        role.setRoleName("roleName:"+new Random().nextInt(100));
        roleService.save(role);
        throw new Exception();
    }

Well, normal rollbacks have been made this time.My God, there is no processing power outside this time. Why accept the errors thrown in it and roll back!!!It looks like things inside and outside always live and die together, right?Originally, @Transactional has another parameter. Look at the source code, this comment also has a default value:

Propagation propagation() default Propagation.REQUIRED;

REQUIRED means that when transactions are nested, if you find that a transaction already exists, you join the transaction instead of creating a new one, so there are no two transactions at all and there is only one!As for the other values of this parameter, we will not test them.Returning to the above question, when an external error occurs, the transaction is viewed without adding the rollbackFor = Exception.class parameter, i.e. without the ability to handle non-RuntimeExceptions, so the code completes, seemingly "two transactions", and rollback fails.When an error occurred inside, the transaction was added the ability to handle non-RuntimeExceptions, so the rollback succeeded when the code was finished.

Conclusion 3: Because of the REQUIRED attribute, "two transactions" is actually a transaction, the processing power depends on the error moment, whether the ability to handle non-RuntimeException s is added.

try catch and transaction nesting affect each other

With the conclusion of one, two, and three, it is much easier to explore issues of common influence, and because there are so many cases, there is not much code to show.

conclusion

Conclusion 1: For @Transactional ly, the rollback of RuntimeException errors can be guaranteed. If you want to guarantee the rollback of non-RuntimeException errors, you need to add the rollbackFor = Exception.class parameter.
Conclusion 2: try catch only affects whether the anomaly can be perceived by @Transactional s.If the error is thrown to a point where the facets can be perceived, it can work.
Conclusion 3: Because of the REQUIRED attribute, "two transactions" is actually a transaction, the processing power depends on the error moment, whether the ability to handle non-RuntimeException s is added.

Topics: Java Attribute Spring