6

引言

最近在撰写论文,参考了大量文献,也在阅读博文的过程中对架构有了新的认识,发现原文章Spring 事务管理因局限于Hibernate框架,未对NESTED级别的事务做详述,特写本文进行补充。

事务管理

Spring 声明式事务

正常的逻辑:

  1. 开启事务
  2. 执行业务代码
  3. 提交或回滚事务

造成了需要编写许多关于事务的冗余代码,为了解决此问题,Spring采用声明式事务。

Spring Boot的核心配置中已经默认启用了事务,使用Transactional注解即为方法添加事务:

image.png

Spring事务注解配置如下,比较主要的就是isolationpropagation了。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

isolation为事务的隔离级别,讲了好多遍了,不做赘述。

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

image.png

propagation为事务传播级别,Spring共配置了7种传播级别,原文章已对前六种做过详述,本文一起来学习Hibernate不支持的NESTED传播级别。

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

基础框架

Hibernate不支持,故本文启用MyBatis进行本传播级别事务的研究。

POM中依赖MyBatisMySQL;为了演示方便,选用了自动化工具mapper-spring-boot-starter

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.1.5</version>
</dependency>

实体层:

public class Cat {

    private Long id;

    private String name;
}

public class Dog {

    private Long id;

    private String name;
}

数据访问层:

public interface CoreMapper<T> extends Mapper<T>, MySqlMapper<T> {
}

@Mapper
public interface CatMapper extends CoreMapper<Cat> {
}

@Mapper
public interface DogMapper extends CoreMapper<Dog> {
}

类似于JPA,对于简单的数据库操作,通过继承MapperMySqlMapper接口,不需要写一行SQL,同时开启驼峰映射,XML也不用写。

image.png

服务层两个保存方法:

@Transactional(propagation = Propagation.NESTED)
@Override
public void save(Cat cat) {
    catMapper.insertUseGeneratedKeys(cat);
}

@Transactional(propagation = Propagation.NESTED)
@Override
public void save(Dog dog) {
    dogMapper.insertUseGeneratedKeys(dog);
}

写个方法测试一下:

public void test() {
    catService.save(new Cat("Hello Kitty"));
    dogService.save(new Dog("史努比"));
}

数据保存成功,数据访问层配置没有问题。

image.png

image.png

NESTED

如果当前存在事务,则在当前事务的一个嵌套事务中运行。

test方法开始事务,调用catdog的保存方法,dog的保存方法中抛出了RuntimeException异常。

@Transactional
public void test() {
    catService.save(new Cat("Hello Kitty"));
    dogService.saveAndThrowException(new Dog("史努比"));
}

执行test方法,两张表的数据都没有存上。不应该是两个子事务吗?dog事务回滚,为什么cat也存不上呢?

image.png

image.png

原因如下,test方法开启了事务,CatServiceDogServiceNESTED的传播级别下分别建立了子事务,嵌套运行,DogService抛出了异常,子事务回滚,不影响父事务。

但是父事务没有捕获RuntimeException,父事务回滚,父事务的回滚会使子事务回滚,所以CatService的子事务也回滚了,造成了两张表的数据都没存上。

image.png

父事务的提交和回滚会使其子事务提交或回滚。

这个层面并不是NESTED的全部,因为全部设置成REQUIRED三个方法共享一个事务也能实现相同的功能。

对上述方法加以修改,添加一个简易的异常处理,再运行。

@Transactional
public void test() {
    catService.save(new Cat("Hello Kitty"));
    try {
        dogService.saveAndThrowException(new Dog("史努比"));
    } catch (RuntimeException e) {
        e.printStackTrace();
    }
}

cat存上了,dog没存上。

image.png

image.png

子事务的提交或回滚不影响父事务的提交或回滚,这里DogService的子事务回滚,向上抛出的异常被处理,父事务不回滚,事务提交。

image.png

与 REQUIRED 比较

学习完特性可能还每碰到过应用场景,我有幸碰到过一次,举例如下:

将事务全部修改为默认的REQUIRED级别重新运行上述代码:

@Transactional
public void test() {
    catService.save(new Cat("Hello Kitty"));
    try {
        dogService.saveAndThrowException(new Dog("史努比"));
    } catch (RuntimeException e) {
        e.printStackTrace();
    }
}

@Transactional
@Override
public void save(Cat cat) {
    catMapper.insertUseGeneratedKeys(cat);
}

@Transactional
@Override
public void saveAndThrowException(Dog dog) {
    this.save(dog);
    throw new RuntimeException();
}

如下图所示,两张表都没存上数据:

image.png

image.png

且控制台报错:

Transaction rolled back because it has been marked as rollback-only.

DogService抛出了异常,将事务标记为回滚,虽然test方法中处理了该异常,但是事务已被标记,导致数据存储失败。

image.png

两相对比之下,NESTED适合允许失败的场景,我遇到的就是软删除场景:

try {
  hardDelete();
} catch(Exception e) {
  softDelete();
}

如果配置为REQUIRED,事务被标记,即使处理异常,仍然回滚,数据软删除失败。此处,可以将hardDelete设置为NESTED,因为该场景下允许hardDelete失败,hardDelete作为子事务,让调用方决定是否回滚。

项目中采用Hibernate,不支持NESTED,为了规避该问题,将传播级别设置为REQUIRES_NEW,挂起当前事务,新建事务进行回滚,不影响调用方的事务。虽然能实现功能,但理论上,还是NESTED更符合逻辑。

开发规范

虽然有这么多隔离级别,但是REQUIREDSUPPORTS已经能满足大多数的开发需求了。

数据库写INSERT/UPDATE/DELETE使用REQUIRED,读SELECT使用SUPPORTS,遇到异常,再分析使用其他事务传播级别。

总结

任何理论都不如现实具体。——沈从文

张喜硕
2.1k 声望423 粉丝

浅梦辄止,书墨未浓。