Spring 事務管理(續)

引言

最近在撰寫論文,參考了大量文獻,也在閱讀博文的過程當中對架構有了新的認識,發現原文章Spring 事務管理因侷限於Hibernate框架,未對NESTED級別的事務作詳述,特寫本文進行補充。java

事務管理

Spring 聲明式事務

正常的邏輯:mysql

  1. 開啓事務
  2. 執行業務代碼
  3. 提交或回滾事務

形成了須要編寫許多關於事務的冗餘代碼,爲了解決此問題,Spring採用聲明式事務。spring

Spring Boot的核心配置中已經默認啓用了事務,使用Transactional註解即爲方法添加事務:sql

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爲事務的隔離級別,講了好多遍了,不作贅述。segmentfault

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傳播級別。mybatis

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-starterapp

<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,遇到異常,再分析使用其餘事務傳播級別。

總結

任何理論都不如現實具體。——沈從文
相關文章
相關標籤/搜索