转载

Spring 事务管理(续)

引言

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

事务管理

Spring 声明式事务

正常的逻辑:

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

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

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

Spring 事务管理(续)

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;
    }
}

Spring 事务管理(续)

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 也不用写。

Spring 事务管理(续)

服务层两个保存方法:

@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("史努比"));
}

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

Spring 事务管理(续)

Spring 事务管理(续)

NESTED

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

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

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

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

Spring 事务管理(续)

Spring 事务管理(续)

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

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

Spring 事务管理(续)

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

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

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

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

cat 存上了, dog 没存上。

Spring 事务管理(续)

Spring 事务管理(续)

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

Spring 事务管理(续)

与 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();
}

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

Spring 事务管理(续)

Spring 事务管理(续)

且控制台报错:

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

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

Spring 事务管理(续)

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

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

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

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

开发规范

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

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

总结

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

原文  https://segmentfault.com/a/1190000022531982
正文到此结束
Loading...