Spring - 12 ( 8000 字 Spring 入门级教程 )

一: Spring 事务

1.1 事务回顾

在数据库阶段我们已经学习过事务的概念。事务是操作的一组集合,是一个不可分割的执行单元。事务将所有操作作为一个整体,要么全部提交到数据库执行成功要么全部撤销以保证数据一致性。换句话说,事务中的所有操作要么同时成功,要么同时失败,事务的操作主要有三步:

操作步骤描述
开启事务使用 start transaction 或 begin,在执行一组操作前开启事务。
提交事务使用 commit,当这组操作全部成功时,将所有更改提交到数据库。
回滚事务使用 rollback,如果这组操作中任何一个操作发生异常,则撤销所有更改,回滚到事务开始前的状态。

Spring 也对事务进行了封装和实现,并提供了两种类型的事务操作:

事务类型描述
编程式事务通过手动编写代码来控制事务的开启、提交和回滚,具有更高的灵活性。
声明式事务通过使用注解(如 @Transactional)自动管理事务的开启、提交和回滚,更加简洁方便。

1.2 Spring 编程式事务

Spring 手动操作事务与 MySQL 操作事务类似,也包括三个关键步骤。SpringBoot 内置了两个重要对象:

组件名称描述
DataSourceTransactionManager用于管理事务的生命周期,包括开启事务、提交事务和回滚事务。
TransactionDefinition定义事务的属性,如隔离级别和传播行为。在获取事务时,需要将 TransactionDefinition 传递给事务管理器,从而获得一个事务对象 TransactionStatus。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.stereotype.Service;

@Service
public class TransactionDemoService {

    @Autowired
    private DataSourceTransactionManager transactionManager;

    public void executeTransaction() {
        // 定义事务属性
        DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
        transactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); // 设置隔离级别
        transactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 设置传播行为

        // 获取事务状态
        TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

        try {
            // 执行业务逻辑
            System.out.println("执行事务中的操作...");

            // 提交事务
            transactionManager.commit(transactionStatus);
            System.out.println("事务提交成功");
        } catch (Exception e) {
            // 回滚事务
            transactionManager.rollback(transactionStatus);
            System.out.println("事务回滚,出现异常:" + e.getMessage());
        }
    }
}

以上代码虽然可以实现事务,但手动管理事务的过程相对繁琐。因此 Spring 提供了声明式事务的方式来自动管理事务,避免手动控制事务的开启、提交和回滚等操作。接下来,我们将学习如何使用声明式事务。

1.3 Spring 声明式事务

声明式事务的实现非常简单,只需在需要事务的方法上添加 @Transactional 注解即可。
Spring 会在方法执行时自动开启事务,方法执行完成后自动提交事务。如果方法执行过程中发生未处理的异常,事务将自动回滚,如果抛出异常了但异常被捕获了则会继续提交。无需手动管理事务的开启、提交或回滚操作,@Transactional 可以用来修饰方法或类。

修饰范围描述
修饰方法仅当修饰 public 方法时事务生效,修饰其他方法不会报错,但事务不会生效,建议优先使用此方式。
修饰类当修饰类时,类中所有的 public 方法都会生效,并自动应用事务管理。
@Slf4j
@RestController
@RequestMapping("/trans")
public class TransactionalController {

    @Autowired
    private UserService userService;

    @Transactional
    @RequestMapping("/registry")
    public String registry(String name, String password) {
        // 用户注册
        userService.registryUser(name, password);
        log.info("用户数据插入成功");

        // 强制程序抛出异常
        int a = 10 / 0;

        return "注册成功";
    }
}

运行程序后发现虽然日志显示数据插入成功,但是数据库中并没有新增数据,因为事务已触发回滚操作。

在这里插入图片描述

@Transactional
@RequestMapping("/registry")
public String registry(String name, String password) {
    // 用户注册
    userService.registryUser(name, password);
    log.info("用户数据插入成功");

    // 对异常进行捕获
    try {
        // 强制程序抛出异常
        int a = 10 / 0;
    } catch (Exception e) {
        e.printStackTrace();
    }

    return "注册成功";
}

运行程序后发现程序发生了错误,但由于异常被捕获,事务未回滚数据仍然被提交。

1.4 @Transactional 详解

通过以上代码,我们了解了 @Transactional 的基本用法。接下来,我们将学习 @Transactional 注解中的一些常用属性:

属性名称描述
rollbackFor指定触发事务回滚的异常类型,可设置多个异常类型。
isolation定义事务的隔离级别,用于控制不同事务之间的隔离行为,默认值为 Isolation.DEFAULT。
propagation定义事务的传播机制,用于控制当前事务如何与其他事务交互,默认值为 Propagation.REQUIRED。

1.4.1 事务回滚的异常类型 rollbackFor

@Transactional 默认只在遇到运行时异常或 Error 时才会触发回滚,而对于非运行时异常默认情况下不会回滚事务,运行时异常是指 RuntimeException 及其子类。

在这里插入图片描述

@Transactional
@RequestMapping("/r2")
public String r2(String name, String password) throws IOException {
    // 用户注册
    userService.registryUser(name, password);
    log.info("用户数据插入成功");

    // 模拟触发异常
    if (true) {
        throw new IOException();
    }

    return "r2";
}

运行程序后发现虽然抛出了异常,但是事务依然进行了提交

在这里插入图片描述

在这里插入图片描述

如果需要所有异常都触发回滚,可以配置 @Transactional 注解中的 rollbackFor 属性,通过该属性指定哪些异常类型会触发事务回滚。

@Transactional(rollbackFor = Exception.class) // 指定当捕获到 Exception 类型及其子类的异常时,事务会自动回滚。
@RequestMapping("/r2")
public String r2(String name, String password) throws IOException {
    // 用户注册
    userService.registryUser(name, password);
    log.info("用户数据插入成功");

    // 模拟触发异常
    if (true) {
        throw new IOException();
    }

    return "r2";
}

1.4.2 事务的隔离级别 isolation

首先回顾一下 MySQL 事务隔离级别:

隔离级别描述常见问题
读未提交 (READ UNCOMMITTED)事务可以读取到其他事务未提交的数据。由于未提交的数据可能回滚,这种数据被称为脏数据。脏读
读提交 (READ COMMITTED)事务只能读取到已经提交的数据,避免了脏读问题。但由于其他事务的提交,导致同一事务多次查询结果不一致。不可重复读
可重复读 (REPEATABLE READ)确保同一事务多次查询的结果一致,即不会读到其他事务修改的数据。但能感知到新插入的数据,可能引发幻读问题。幻读(MySQL 默认级别)
串行化 (SERIALIZABLE)最高隔离级别,通过强制事务排序解决脏读、不重复读和幻读问题,但执行效率低,应用场景较少。无常见问题

脏读指的是一个事务读取了另一个未提交的事务修改的数据。如果那个未提交的事务后来回滚了,那么第一个事务读取到的数据就是无效的,也就是“脏数据”。例如,事务A修改了一个数据但还没提交,事务B读取了这个未提交的数据,之后事务A回滚,导致事务B读取的数据是错误的。

不可重复读指的是在一个事务内,多次读取同一数据,但结果不同。这是因为在两次读取之间,另一个事务修改了该数据并提交了。例如,事务A第一次读取数据X,之后事务B修改了X并提交,事务A再次读取X时发现值变了,导致不可重复读。

幻读则是指在同一事务中,多次查询同一范围的记录时,后一次查询看到了前一次查询未看到的行。通常是因为另一个事务在这期间插入了新数据。例如,事务A连续查询几次某个条件,第一次查询得到N条记录,此时事务B插入了一条符合该条件的新记录并提交,接着事务A第二次查询时突然发现多了一条记录,就像出现了幻觉一样。

读未提交:最低的隔离级别,允许读取未提交的数据变更。这种情况下,脏读、不可重复读、幻读都可能发生。它没有解决任何问题。

读提交:事务只能读取已经提交的数据。这解决了脏读的问题,因为未提交的数据不会被其他事务读取到。但不可重复读和幻读仍然可能发生,因为其他事务可能在当前事务的两次读取之间提交了修改或新增的数据。

可重复读:确保同一事务多次读取同一数据的结果一致。通常通过锁定读取的数据行来实现,这样在事务结束前,其他事务不能修改这些数据。这解决了不可重复读的问题,但幻读仍然可能发生,因为其他事务可能插入新的数据行,只是不能修改原有的数据行。

串行化:最高的隔离级别,强制事务串行执行,避免了并发问题。通过锁表的方式,确保同一时间只有一个事务可以操作数据,解决了脏读、不可重复读和幻读的问题。但这样会导致并发性能大幅下降,因为事务需要排队执行。

在这里插入图片描述

Spring 提供了 5 种事务隔离级别,可以通过 @Transactional 注解的 isolation 属性进行设置。

隔离级别描述
Isolation.DEFAULT默认隔离级别,使用数据库连接的事务隔离级别为准。
Isolation.READ_UNCOMMITTED读未提交,对应 SQL 标准中的 READ UNCOMMITTED。
Isolation.READ_COMMITTED读已提交,对应 SQL 标准中的 READ COMMITTED。
Isolation.REPEATABLE_READ可重复读,对应 SQL 标准中的 REPEATABLE READ。
Isolation.SERIALIZABLE串行化,对应 SQL 标准中的 SERIALIZABLE,事务隔离级别最高,执行效率较低。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/transaction")
public class TransactionController {

    @Autowired
    private UserService userService;

    @Transactional(isolation = Isolation.REPEATABLE_READ)
    @RequestMapping("/isolationTest")
    public String isolationTest(String name, String password) {
        // 插入用户数据
        userService.registryUser(name, password);
        System.out.println("用户数据已插入");

        // 模拟读取数据的操作
        String userInfo = userService.getUserInfo(name);
        System.out.println("读取用户信息:" + userInfo);

        return "事务隔离级别测试完成";
    }
}

1.4.3 事务的传播机制 propagation

事务传播机制是指在多个事务方法存在调用关系时,事务如何在这些方法间传播。例如,当方法 A 和方法 B 都被 @Transactional 修饰时,方法 A 执行时会开启一个事务。当方法 A 调用方法 B 时,由于方法 B 也有事务,此时方法 B 的执行是加入方法 A 的事务,还是创建一个新的事务呢?此时就取决于事务的传播机制。

事务隔离级别主要解决的是多个事务同时访问同一个数据库时的数据一致性问题,而事务传播机制解决的是单个事务在多个方法或节点之间传递时的行为控制问题。

在这里插入图片描述
在这里插入图片描述
@Transactional 注解支持通过设置 propagation 属性来指定事务的传播行为。Spring 提供以下 7 种事务传播机制:

传播行为描述类比
Propagation.REQUIRED默认传播级别。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。需要有房子。如果你有房,我们就一起住;如果你没房,我们就一起买房。
Propagation.SUPPORTS如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行,非事务运行就相当于运行了个普通方法。可以有房子。如果你有房,那就一起住;如果没房,那就租房。
Propagation.MANDATORY强制性。如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。必须有房子。要求必须有房,如果没房就不结婚。
Propagation.REQUIRES_NEW创建一个新的事务。如果当前存在事务,则挂起当前事务。内部方法的新事务与外部事务互相独立,互不干扰。必须买新房。不管你有没有房,必须一起买房,即使有房也不住。
Propagation.NOT_SUPPORTED以非事务方式运行。如果当前存在事务,则挂起当前事务。不需要房。不管你有没有房,我都不住,必须租房。
Propagation.NEVER以非事务方式运行。如果当前存在事务,则抛出异常。不能有房子。如果你有房,就不行,抛出异常。
Propagation.NESTED如果当前存在事务,则创建一个嵌套事务作为当前事务的子事务运行;当不存在外部事务时,NESTED会像REQUIRED一样创建一个新事务,而不是嵌套事务。如果你没房,就一起买房;如果你有房,我们就以房子为根据地,做点小生意。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class RequiredPropagationExample {

    @Autowired
    private SubService subService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void mainServiceMethod() {
        System.out.println("主服务方法开始...");
        
        // 模拟插入操作
        System.out.println("主服务插入一条记录");
        
        // 调用子服务方法
        subService.subServiceMethod();

        System.out.println("主服务方法结束...");
    }
}

@Service
class SubService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void subServiceMethod() {
        System.out.println("子服务方法开始...");
        
        // 模拟插入操作
        System.out.println("子服务插入一条记录");

        // 模拟异常
        if (true) {
            throw new RuntimeException("子服务方法抛出异常,事务回滚");
        }

        System.out.println("子服务方法结束...");
    }
}
主服务方法开始...
主服务插入一条记录
子服务方法开始...
子服务插入一条记录
Exception in thread "main" java.lang.RuntimeException: 子服务方法抛出异常,事务回滚

主服务方法 mainServiceMethod 和子服务方法 subServiceMethod 都使用了 Propagation.REQUIRED。当主服务方法调用子服务方法时,子服务方法会加入主服务方法的事务,共享同一个事务。由于子服务方法抛出异常导致整个事务都会回滚,因此数据库中不会插入任何记录。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ice___Cpu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值