我们好,我是树哥。

Spring 业务是复杂一致性业务必备的知识点,掌握好 Spring 业务能够让我们写出更好地代码。这篇文章我们将介绍 Spring 业务的诞生布景,然后让我们能够更明晰地了解 Spring 业务存在的意义。

接着,我们会介绍怎样快速运用 Spring 业务。接着,我们会介绍 Spring 业务的一些特性,然后帮助我们更好地运用 Spring 业务。最终,我们会总结一些 Spring 业务常见的问题,防止我们踩坑。

深入理解 Spring 事务:入门、使用、原理

诞生布景

当我们聊起业务的时分,我们需求明白「业务」这个词代表着什么。

业务其实是一个并发操控单位,是用户定义的一个操作序列,这些操作要么悉数完结,要不悉数不完结,是一个不可分割的工作单位。业务有 ACID 四个特性,即:

  1. Atomicity(原子性):业务中的一切操作,或者悉数完结,或者悉数不完结,不会结束在中间某个环节。
  2. 一致性(Consistency):在业务开始之前和业务结束今后,数据库的完整性没有被破坏。
  3. 业务隔离(Isolation):多个业务之间是独立的,不相互影响的。
  4. 持久性(Durability):业务处理结束后,对数据的修正便是永久的,即使系统故障也不会丢掉。

而我们说的 Spring 业务,其实是业务在 Spring 中的完结。

明白了什么是业务之后,我们来聊聊:为什么要有 Spring 业务?

为了解说清楚这个问题,我们举个简略的比如:银行里树哥要给小黑转 1000 块钱,这时分会有两个必要的操作:

  1. 将树哥的账户余额削减 1000 元。
  2. 将小黑的账户余额添加 1000 元。

这两个操作,要么一起都完结,要么都不完结。假如其中某个成功,别的一个失利,那么就会呈现严重的问题。而我们要保证这个操作的原子性,就有必要经过 Spring 业务来完结,这便是 Spring 业务存在的原因。

假如你深化了解过 MySQL 业务,那么你应该知道:MySQL 默许情况下,关于一切的单条语句都作为一个独自的业务来履行。我们要运用 MySQL 业务的时分,能够经过手动提交业务来操控业务范围。Spring 业务的本质,其实便是经过 Spring AOP 切面技能,在合适的当地敞开业务,接着在合适的当地提交业务或回滚业务,然后完结了业务编程层面的业务操作。

运用指南

Spring 业务支持两种运用办法,分别是:声明式业务(注解办法)、编程式业务(代码办法)。一般来说,我们运用声明式业务比较多,这儿我们就演示声明式业务的运用办法。

项目预备

为了较好地进行讲解,我们需求搭建一个具备数据库 CURD 功用的项目,并创立 tablea 和 tableb 两张表。

首要,创立 tablea 和 tableb 两张表,两张表都只要 id 和 name 两列,建表语句如下图所示。

CREATE TABLE `tablea` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1;
CREATE TABLE `tableb` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1;

接着,创立一个 SpringBoot 项目,随后参加 MyBatis 及 MySQL 的 POM 依赖。

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.1.0</version>
</dependency>

最终,我们创立对应的 controller 接口、service 接口、mapper 接口,代码如下所示。

创立 controller 接口:

@SpringBootApplication
@RestController
@RequestMapping("/api")
public class SpringTransactionController {
    @Autowired
    private TransactionServiceA transactionServiceA;
    @RequestMapping("/spring-transaction")
    public String testTransaction() {
        transactionServiceA.methodA();
        return "SUCCESS";
    }
}

创立 TableService 接口。

public interface TableService {
    void insertTableA(TableEntity tableEntity);
    void insertTableB(TableEntity tableEntity);
}

创立 Service 接口完结类 TransactionServiceA 类,在 methodA() 办法中先往 tablea 表格刺进一条数据,随后会调用 TransactionServiceB 服务的 methodB() 办法。

@Service
public class TransactionServiceA {
    @Autowired
    private TableService tableService;
    @Autowired
    private TransactionServiceB transactionServiceB;
    public void methodA(){
        System.out.println("methodA");
        tableService.insertTableA(new TableEntity());
        transactionServiceB.methodB();
    }
}

创立 TransactionServiceB 类完结,在 methodB() 办法中往 tableb 表格刺进一条数据。

@Service
public class TransactionServiceB {
    @Autowired
    private TableService tableService;
    public void methodB(){
        System.out.println("methodB");
        tableService.insertTableB(new TableEntity());
    }
}

创立 Mapper 接口办法:

@Mapper
public interface TableMapper {
    @Insert("INSERT INTO tablea(id, name) " +
            "VALUES(#{id}, #{name})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insertTableA(TableEntity tableEntity);
    @Insert("INSERT INTO tableb(id, name) " +
            "VALUES(#{id}, #{name})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insertTableB(TableEntity tableEntity);
}

数据库表对应的 TableEntity:

@Data
public class TableEntity {
    private static final long serialVersionUID = 1L;
    private Long id;
    private String name;
    public TableEntity() {
    }
    public TableEntity(String name) {
        this.name = name;
    }
}

最终,我们在装备文件中装备好数据库地址:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis 装备
mybatis:
  type-aliases-package: tech.shuyi.javacodechip.spring_transaction.model
  configuration:
    map-underscore-to-camel-case: true

最终,我们运行 SpringBoot 项目。经过浏览器访问地址:localhost:8080/api/spring-transaction,正常的话应该是接口恳求成功。

深入理解 Spring 事务:入门、使用、原理

检查数据库表,会看到 tablea 和 tableb 都刺进了一条数据。

到这儿,我们用于测验 Spring 业务的 Demo 就预备结束了!

快速入门

运用声明式业务的办法很简略,其实便是在 Service 层对应办法上装备 @Transaction 注解即可。

假设我们的业务需求是:往 tablea 和 tableb 刺进的数据,要么都完结,要么都不完结。

这时分,我们应该怎样操作呢?

首要,我们需求在 TransactionServiceA 类的 methodA() 办法上装备 @Transaction 注解,一起也在 TransactionServiceB 类的 methodB() 办法上装备 @Transaction 注解。修正之后的 TransactionServiceA 和 TransactionServiceB 代码如下所示。

// TransactionServiceA
@Transactional
public void methodA(){
    System.out.println("methodA");
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
}
// TransactionServiceB
@Transactional
public void methodB(){
    System.out.println("methodB");
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

能够看到,我们在 methodB() 中模拟了业务反常,我们看看是否 tablea 和 tableb 都没有刺进数据。

修正之后重新发动项目,此刻我们持续访问地址:localhost:8080/api/spring-transaction,我们会发现履行过错,而且操控台也报错了。

深入理解 Spring 事务:入门、使用、原理

深入理解 Spring 事务:入门、使用、原理

这时分我们检查数据库,会发现 tablea 和 tableb 都没有刺进数据。这说明业务起作用了。

业务传达类型

业务传达类型,指的是业务与业务之间的交互战略。例如:在业务办法 A 中调用业务办法 B,当业务办法 B 失利回滚时,业务办法 A 应该怎样操作?这便是业务传达类型。Spring 业务中定义了 7 种业务传达类型,分别是:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。其中最常用的只要 3 种,即:REQUIRED、REQUIRES_NEW、NESTED。

针对业务传达类型,我们要弄明白的是 4 个点:

  1. 子业务与父业务的关系,是否会发动一个新的业务?
  2. 子业务反常时,父业务是否会回滚?
  3. 父业务反常时,子业务是否会回滚?
  4. 父业务捕捉反常后,父业务是否还会回滚?

REQUIRED

REQUIRED 是 Spring 默许的业务传达类型,该传达类型的特色是:当时办法存在业务时,子办法参加该业务。此刻父子办法共用一个业务,不管父子办法哪个产生反常回滚,整个业务都回滚。即使父办法捕捉了反常,也是会回滚。而当时办法不存在业务时,子办法新建一个业务。 为了验证 REQUIRED 业务传达类型的特色,我们来做几个测验。

还是上面 methodA 和 methodB 的比如。当 methodA 不敞开业务,methodB 敞开业务,这时分 methodB 便是独立的业务,而 methodA 并不在业务之中。因此当 methodB 产生反常回滚时,methodA 中的内容就不会被回滚。用如下的代码就能够验证我们所说的。

public void methodA(){
    System.out.println("methodA");
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
}
@Transactional
public void methodB(){
    System.out.println("methodB");
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

最终的成果是:tablea 刺进了数据,tableb 没有刺进数据,符合了我们的猜测。

当 methodA 敞开业务,methodB 也敞开业务。依照我们的结论,此刻 methodB 会参加 methodA 的业务。此刻,我们验证当父子业务分别回滚时,别的一个业务是否会回滚。

我们先验证第一个:当父办法业务回滚时,子办法业务是否会回滚?

@Transactional
public void methodA(){
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
    throw new RuntimeException();
}
@Transactional
public void methodB(){
    tableService.insertTableB(new TableEntity());
}

成果是:talbea 和 tableb 都没有刺进数据,即:父业务回滚时,子业务也回滚了。

我们持续验证第二个:当子办法业务回滚时,父办法业务是否会回滚?

@Transactional
public void methodA(){
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
}
@Transactional
public void methodB(){
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

成果是:talbea 和 tableb 都没有刺进数据,即:子业务回滚时,父业务也回滚了。

我们持续验证第三个:当字办法业务回滚时,父办法捕捉了反常,父办法业务是否会回滚?

@Transactional
public void methodA() {
    tableService.insertTableA(new TableEntity());
    try {
        transactionServiceB.methodB();
    } catch (Exception e) {
        System.out.println("methodb occur exp.");
    }
}
@Transactional
public void methodB() {
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

成果是:talbea 和 tableb 都没有刺进数据,即:子业务回滚时,父业务也回滚了。所以说,这也进一步验证了我们之前所说的:REQUIRED 传达类型,它是父子办法共用同一个业务的。

REQUIRES_NEW

REQUIRES_NEW 也是常用的一个传达类型,该传达类型的特色是:不管当时办法是否存在业务,子办法都新建一个业务。此刻父子办法的业务时独立的,它们都不会相互影响。但父办法需求留意子办法抛出的反常,防止因子办法抛出反常,而导致父办法回滚。 为了验证 REQUIRES_NEW 业务传达类型的特色,我们来做几个测验。

首要,我们来验证一下:当父办法业务产生反常时,子办法业务是否会回滚?

@Transactional
public void methodA(){
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
    throw new RuntimeException();
}
    @Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
    tableService.insertTableB(new TableEntity());
}

成果是:tablea 没有刺进数据,tableb 刺进了数据,即:父办法业务回滚了,但子办法业务没回滚。这能够证明父子办法的业务是独立的,不相互影响。

下面,我们来看看:当子办法业务产生反常时,父办法业务是否会回滚?

@Transactional
public void methodA(){
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
}
    @Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

成果是:tablea 没有刺进了数据,tableb 没有刺进数据。

从这个成果来看,貌似是子办法业务回滚,导致父办法业务也回滚了。但我们不是说父子业务都是独立的,不会相互影响么?怎样成果与此相反呢?

其实是由于子办法抛出了反常,而父办法并没有做反常捕捉,此刻父办法一起也抛出反常了,所以 Spring 就会将父办法业务也回滚了。假如我们在父办法中捕捉反常,那么父办法的业务就不会回滚了,修正之后的代码如下所示。

@Transactional
public void methodA(){
    tableService.insertTableA(new TableEntity());
    // 捕捉反常
    try {
        transactionServiceB.methodB();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
    @Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

成果是:tablea 刺进了数据,tableb 没有刺进数据。这正符合我们刚刚所说的:父子业务是独立的,并不会相互影响。

这其实便是我们上面所说的:父办法需求留意子办法抛出的反常,防止因子办法抛出反常,而导致父办法回滚。由于假如履行过程中产生 RuntimeException 反常和 Error 的话,那么 Spring 业务是会自动回滚的。

NESTED

NESTED 也是常用的一个传达类型,该办法的特性与 REQUIRED 十分相似,其特性是:当时办法存在业务时,子办法参加在嵌套业务履行。当父办法业务回滚时,子办法业务也跟着回滚。当子办法业务发送回滚时,父业务是否回滚取决所以否捕捉了反常。假如捕捉了反常,那么就不回滚,不然回滚。

能够看到 NESTED 与 REQUIRED 的区别在于:父办法与子办法关于共用业务的描述是不一样的,REQUIRED 说的是共用同一个业务,而 NESTED 说的是在嵌套业务履行。这一个区别的具体体现是:在子办法业务产生反常回滚时,父办法有着不同的反响动作。

关于 REQUIRED 来说,不管父子办法哪个产生反常,全都会回滚。而 REQUIRED 则是:父办法产生反常回滚时,子办法业务会回滚。而子办法业务发送回滚时,父业务是否回滚取决所以否捕捉了反常。

为了验证 NESTED 业务传达类型的特色,我们来做几个测验。

首要,我们来验证一下:当父办法业务产生反常时,子办法业务是否会回滚?

@Transactional
public void methodA() {
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
    throw new RuntimeException();
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
    tableService.insertTableB(new TableEntity());
}

成果是:tablea 和 tableb 都没有刺进数据,即:父子办法业务都回滚了。这说明父办法发送反常时,子办法业务会回滚。

接着,我们持续验证一下:当子办法业务产生反常时,假如父办法没有捕捉反常,父办法业务是否会回滚?

@Transactional
public void methodA() {
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

成果是:tablea 和 tableb 都没有刺进数据,即:父子办法业务都回滚了。这说明子办法发送反常回滚时,假如父办法没有捕捉反常,那么父办法业务也会回滚。

最终,我们验证一下:当子办法业务产生反常时,假如父办法捕捉了反常,父办法业务是否会回滚?

@Transactional
public void methodA() {
    tableService.insertTableA(new TableEntity());
    try {
        transactionServiceB.methodB();
    } catch (Exception e) {
    }
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

成果是:tablea 刺进了数据,tableb 没有刺进数据,即:父办法业务没有回滚,子办法业务回滚了。这说明子办法发送反常回滚时,假如父办法捕捉了反常,那么父办法业务就不会回滚。

看到这儿,相信我们已经对 REQUIRED、REQUIRES_NEW 和 NESTED 这三个传达类型有了深化的了解了。最终,让我们来总结一下:

业务传达类型 特性
REQUIRED 当时办法存在业务时,子办法参加该业务。此刻父子办法共用一个业务,不管父子办法哪个产生反常回滚,整个业务都回滚。即使父办法捕捉了反常,也是会回滚。而当时办法不存在业务时,子办法新建一个业务。
REQUIRES_NEW 不管当时办法是否存在业务,子办法都新建一个业务。此刻父子办法的业务时独立的,它们都不会相互影响。但父办法需求留意子办法抛出的反常,防止因子办法抛出反常,而导致父办法回滚。
NESTED 当时办法存在业务时,子办法参加在嵌套业务履行。当父办法业务回滚时,子办法业务也跟着回滚。当子办法业务发送回滚时,父业务是否回滚取决所以否捕捉了反常。假如捕捉了反常,那么就不回滚,不然回滚。

运用办法论

看完了业务的传达类型,我们对 Spring 业务又有了深刻的了解。

看到这儿,你应该也明白:运用业务,不再是简略地运用 @Transaction 注解就能够,还需求依据业务场景,选择合适的传达类型。那么我们再提高一下运用 Spring 业务的办法论。一般来说,运用 Spring 业务的过程为:

  1. 依据业务场景,分析要达成的业务作用,确认运用的业务传达类型。
  2. 在 Service 层运用 @Transaction 注解,装备对应的 propogation 特点。

下次遇到要运用业务的情况,记住依照这样的过程去做哦~

Spring 业务失效

  1. 什么时分 Spring 业务会失效?

若同一类中的其他没有 @Transactional 注解的办法内部调用有 @Transactional 注解的办法,有 @Transactional 注解的办法的业务会失效。

这是由于 Spring AOP 代理的原因形成的,由于只要当 @Transactional 注解的办法在类以外被调用的时分,Spring 业务办理才收效。

别的,假如直接调用,不经过对象调用,也是会失效的。由于 Spring 业务是经过 AOP 完结的。

@Transactional 注解只要作用到 public 办法上业务才收效。

被 @Transactional 注解的办法所在的类有必要被 Spring 办理。

底层运用的数据库有必要支持业务机制,不然不收效。

彩蛋

Spring 业务履行过程中,假如抛出非 RuntimeException 和非 Error 过错的其他反常,那么是不会回滚的哦。例如下面的代码履行后,tablea 和 tableb 两个表格,都会刺进一条数据。

@Transactional
public void methodA() throws Exception {
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
}
@Transactional
public void methodB() throws Exception {
    tableService.insertTableB(new TableEntity());
    // 非 RuntimeException
    throw new Exception();
}

参考资料

  • 我们自始至终说一次 Spring 业务办理(器) – SegmentFault 思否
  • 【技能干货】Spring业务原理一探 – 知乎
  • Spring 业务详解 | JavaGuide
  • 业务之六:spring 嵌套业务 – duanxz – 博客园
  • 比如很具体,不错!VIP!NESTED 区别!spring 业务传达行为详解 – 双间 – 博客园
  • Spring Boot 实战 —— MyBatis(注解版)运用办法 | Michael 翔
  • 记一次业务的坑 Transaction rolled back because it has been marked as rollback-only – 云扬四海