【Spring Boot】事务的隔离级别与事务的传播特性详解:如何在 Spring 中使用事务?不同隔离级别的区别?


1 事务

1.1 事务简介与 mysql 中的事务使用

事务这个词在学习 MySQL 和多线程并发编程的时候,想必大家或多或少接触过。那么什么是事务呢?

事务是指一组操作作为一个不可分割的执行单元,要么全部成功执行,要么全部失败回滚。在数据库中,事务可以保证数据的一致性、完整性和稳定性,同时避免了数据的异常和不一致情况。常见的事务包括插入、更新、删除等数据库操作。事务的核心要素是ACID特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。

比如,常见的转账操作,以小明给小红转账100元为例,分为如下两个操作:

  1. 小明的账户 -100元;
  2. 小红的账户 +100元。

如果没有事务,第一步操作执行成功,而第二步执行失败,就会导致小明账户平白无故的扣款而小红账户没有收到款项的问题。因此,事务的存在是必要的,这一组操作要么全部执行成功,要么一起失败~
转账示意图

在 MySQL 中,事务有三个重要的操作,分别为:开启事务、提交事务、回滚事务,对应的操作命令如下:

-- 开启事务
start transaction;
-- 业务执行
...
-- 提交事务
commit;
-- 回滚事务
rollback;

1.2 Spring 编程式事务(手动操作)

与 MySQL 操作事务类似,Spring 手动操作事务也需要三个重要的操作:开启事务(获取事务)、提交事务、回滚事务。

SpringBoot 内置了两个对象:

  • DataSourceTransactionManager ⽤来获取事务(开启事务)、提交或回滚事务的;
  • TransactionDefinition 是事务的属性,在获取事务的时候需要将TransactionDefinition 传递进去从⽽获得⼀个事务 TransactionStatus

实现代码如下:

@RestController
@RequestMapping("/user")
public class UserController {
    
    

    @Autowired
    private UserService userService;
    // 事务管理器
    @Autowired
    private DataSourceTransactionManager transactionManager;
    // 定义事务属性
    @Autowired
    private TransactionDefinition transactionDefinition;

    @RequestMapping("/add")
    public int add(UserInfo userInfo) {
    
    
        // 非空校验
        if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
            || !StringUtils.hasLength(userInfo.getPassword())) {
    
    
            return 0;
        }
        // 1. 开始事务
        TransactionStatus transactionStatus =
                transactionManager.getTransaction(transactionDefinition);
        int result = userService.add(userInfo);
        System.out.println("添加: " + result);
//        // 2. 回滚事务
//        transactionManager.rollback(transactionStatus);
        // 3. 提交事务
        transactionManager.commit(transactionStatus);
        return result;
    }
}

从上述代码可以看出,虽然可以实现事务,但是操作很繁琐。因此,我们 常常使用另一种更简单的方式:基于注解的声明式事务。

1.3 Spring 声明式事务(自动操作)

相比手动操作事务来说,声明式事务非常简单,只需要在需要的方法上添加 @Transactional 注解,无需手动开启事务和提交事务。

示例代码如下:

@Transactional // 声明式事务(自动提交)
@RequestMapping("/insert")
public Integer insert(UserInfo userInfo) {
    
    
    // 非空校验
    if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
            || !StringUtils.hasLength(userInfo.getPassword())) {
    
    
        return 0;
    }
    int result = userService.add(userInfo);
    return result;
}

对于 @Transactional 的几点说明:

  1. 该注解可以加在方法或者类上,若加在类上,则说明该类的所有公共方法可以自动的开启和提交事务 ,无论修饰方法还是类,都只对 public 方法有效;
  2. 在方法执行前自动开启事务,在方法执行完毕(没有发生任何异常)自动提交事务。如果 在方法执行期间出现异常,会自动回滚事务。

附:@Transactional 的常见参数:

参数 说明
propagation 定义了事务方法被嵌套调用时,事务如何传播到被调用的方法。常见取值包括:
- REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- REQUIRES_NEW:每次调用方法时都会创建一个新的事务,如果存在当前事务,则将其挂起。
- SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
- NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则将其挂起。
- MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- NEVER:以非事务方式执行操作,如果当前存在事务,则抛出异常。
- NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则创建一个新的事务。
isolation 定义了事务并发执行时,事务之间的隔离程度。常见取值包括:
- DEFAULT(默认):使用数据库默认的隔离级别。
- READ_UNCOMMITTED:最低的隔离级别,事务可以读取未提交的数据。
- READ_COMMITTED:事务只能读取已提交的数据。
- REPEATABLE_READ:事务在整个过程中保持一致的读取视图,防止脏读和不可重复读。
- SERIALIZABLE:最高的隔离级别,事务串行执行,避免脏读、不可重复读和幻读。
timeout 定义了事务执行的最长时间,单位为秒。默认值为-1,表示没有超时限制。
readOnly 如果设置为true,表示事务只读,不会修改数据库的数据。默认值为false
rollbackFor 触发事务回滚的异常类数组。当方法抛出指定的异常时,事务将回滚。
noRollbackFor 不触发事务回滚的异常类数组。当方法抛出指定的异常时,事务将不会回滚。
rollbackForClassName 触发事务回滚的异常类名数组。当方法抛出指定的异常时,事务将回滚。
noRollbackForClassName 不触发事务回滚的异常类名数组。当方法抛出指定的异常时,事务将不会回滚。
value 用于指定事务管理器的名称。如果应用程序中存在多个事务管理器,可以使用该参数指定要使用的事务管理器的名称。默认情况下,事务将使用默认的事务管理器。
transactionManager 用于指定事务管理器的引用。可以直接将一个事务管理器对象传递给该参数,以指定要使用的事务管理器。默认情况下,事务将使用默认的事务管理器。

对于上述表格中的事务隔离级别需要重点掌握,具体后面详细说。

需要特别注意的是,如果方法中的异常被 try-catch 异常捕获处理后,则不会再进行事务的回滚。

当然,我们可以通过 throw 将异常抛出,使得事务能够正常自动回滚。但是这样子做,try-catch 还有意义吗?表情包

因此,对于这种情况,更偏向于使用另一种优雅的方式,进行手动回滚事务来解决~

如何在声明式事务中进行手动回滚事务?
使用代码进行手动回滚事务:

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

示例代码如下:
代码示例

1.4 @Transactional 的工作原理

  1. 当调用被@Transactional注解标记的方法时,事务管理器会检查当前是否存在一个事务。如果存在事务,则该方法将在该事务的上下文中执行;如果不存在事务,则会创建一个新的事务。
  2. 在方法执行期间,如果发生了受检查异常(checked exception),事务管理器会捕获该异常,并根据配置的回滚规则决定是否回滚事务。如果异常被捕获并且需要回滚事务,则事务将被回滚,方法的执行将终止,并将异常传播给调用方。
  3. 如果方法成功执行并且没有抛出受检查异常,事务管理器将提交事务,将数据库中的更改持久化。
  4. 如果方法执行期间抛出了未受检查异常(unchecked exception)或错误(Error),事务管理器会将事务标记为回滚,并将异常传播给调用方。
  5. 如果方法执行期间没有抛出异常,但在方法内部调用了其他被@Transactional注解标记的方法,事务管理器将根据事务的传播行为决定如何处理这些方法。例如,如果传播行为设置为REQUIRED,则内部方法将加入当前事务;如果传播行为设置为REQUIRES_NEW,则内部方法将创建一个新的事务。

具体来看,@Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。@Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务。实现细节的执行流程如图所示:
实现细节


2 事务的隔离级别

2.1 事务的四大特性及事务的隔离级别回顾

事务有4 ⼤特性(ACID),原⼦性、⼀致性、隔离性和持久性:

  • 原⼦性: ⼀个事务中的所有操作,要么全部完成,要么全部不完成。若事务在执⾏过程中发⽣错误,会被回滚到事务开始前的状态。
  • ⼀致性: 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。即写⼊的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以⾃发性地完成预定的⼯作。
  • 持久性: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
  • 隔离性: 数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可以防⽌多个事务并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。

其中,对于隔离性有隔离级别可以设置。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串⾏化(Serializable)。

归根到底,事务隔离级别的设置是为了防止其它事务影响当前事务的一种策略。

在MySQL中,默认是可重复读(repeatable read)级别。以下是MySQL常见的事务隔离级别以及它们对脏读、不可重复读和幻读问题的解决情况的表格:

事务隔离级别 脏读(Dirty Read) 不可重复读(Non-repeatable Read) 幻读(Phantom Read)
读未提交(Read Uncommitted) 可能发生 可能发生 可能发生
读已提交(Read Committed) 避免 可能发生 可能发生
可重复读(Repeatable Read) 避免 避免 可能发生
串行化(Serializable) 避免 避免 避免

对于脏读、不可重复读和幻读的解释:

  • 脏读(Dirty Read):指一个事务读取了另一个事务未提交的数据。如果一个事务可以读取未提交的数据,则会发生脏读。

  • 不可重复读(Non-repeatable Read):指在同一个事务中,多次读取同一行数据时,得到的结果不一致。这是因为在读取期间,另一个事务修改了该行数据。

  • 幻读(Phantom Read):指在同一个事务中,多次查询同一个范围的数据时,得到的结果集不一致。这是因为在查询期间,另一个事务插入或删除了符合查询条件的数据。

每个隔离级别对这些问题的解决情况如下:

  • 读未提交(Read Uncommitted):允许脏读、不可重复读和幻读。一个事务可以读取另一个事务未提交的数据。

  • 读已提交(Read Committed):避免脏读。一个事务只能读取已提交的数据。但是,可能发生不可重复读和幻读,因为在同一个事务中,另一个事务可能会修改数据。

  • 可重复读(Repeatable Read):避免脏读和不可重复读。在同一个事务中,多次读取同一行数据时,得到的结果是一致的。但是,可能发生幻读,因为在同一个事务中,另一个事务可能会插入或删除数据。

  • 串行化(Serializable):避免脏读、不可重复读和幻读。事务串行执行,保证了数据的一致性和完整性。

但隔离级别的提升会增加并发性能的开销,因为更高的隔离级别通常需要使用锁来实现。

在数据库中,可以使用如下语句来查询全局事务隔离级别和当前连接的事务隔离级别:

select @@global.tx_isolation,@@tx_isolation;

事务隔离级别查询

2.2 Spring 事务的隔离级别及设置

在Spring框架中,事务的隔离级别可以使用@Transactional注解来设置。@Transactional注解可以应用在方法级别或类级别上,用于声明一个事务性方法或类。

Spring 框架支持以下五个事务隔离级别:

  1. DEFAULT(默认):使用底层数据库的默认隔离级别。对于大多数数据库来说,通常是READ_COMMITTED

  2. READ_UNCOMMITTED:读未提交。允许脏读、不可重复读和幻读。这是最低的隔离级别,一个事务可以读取另一个事务未提交的数据。

  3. READ_COMMITTED:读已提交。避免脏读。一个事务只能读取已提交的数据。但是,可能发生不可重复读和幻读,因为在同一个事务中,另一个事务可能会修改数据。

  4. REPEATABLE_READ:可重复读。避免脏读和不可重复读。在同一个事务中,多次读取同一行数据时,得到的结果是一致的。但是,可能发生幻读,因为在同一个事务中,另一个事务可能会插入或删除数据。

  5. SERIALIZABLE:串行化。避免脏读、不可重复读和幻读。事务串行执行,保证了数据的一致性和完整性,但是性能太低。

可以在@Transactional注解上使用isolation属性来设置事务的隔离级别。例如:

@Transactional(isolation = Isolation.READ_COMMITTED)
public void myTransactionalMethod() {
    
    
    // 事务性操作
}

需要注意的是,事务的隔离级别还受数据库本身支持的隔离级别的限制。如果数据库不支持某个特定的隔离级别,那么Spring框架将尽力使用最接近的隔离级别。


3 Spring 事务传播机制

3.1 初探事务的传播机制

事务的传播机制是用来定义事务在传播过程中的行为模式的一种机制。 Spring 事务传播机制定义了多个包含了事务的方法,相互调用时,事务是如何在这些方法进行传递的。

对比事务的隔离级别来看,如果说事务的隔离级别是保证多个并发事务执行的可控性的(稳定性),则 事务的传播机制就是保证一个事务在多个调用方法间的可控性的(稳定性)。

事务的隔离级别解决的是多个并发事务调用数据库的问题:
事务隔离级别

事务的传播机制解决的是一个事务在多个节点(方法)中传递的问题:
事务的传播机制
比如,方法 A 正常执行,完成了事务。但是,方法 B 发生了错误。那么,方法 A 进行的事务操作是否要回滚呢?这就是事务的传播机制需要解决的问题~

3.2 Spring 事务传播机制的分类及设置

在Spring框架中,事务传播机制用于定义在多个事务性方法相互调用时,事务如何传播和交互的规则。Spring框架提供了七种不同的事务传播行为:

  1. REQUIRED(需要有):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是最常用的传播行为。

  2. SUPPORTS(可以有):如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。

  3. MANDATORY(强制有):如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

  4. REQUIRES_NEW:创建一个新的事务,并挂起当前事务(如果存在)。新创建的事务与当前事务完全独立。

  5. NOT_SUPPORTED:以非事务方式执行,并且挂起当前事务(如果存在)。

  6. NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

  7. NESTED:如果当前存在事务,则在嵌套事务中执行。嵌套事务是独立于当前事务的子事务,它可以独立地进行提交或回滚。如果当前没有事务,则创建一个新的事务。

这些事务传播行为可以通过@Transactional注解的propagation属性进行设置。例如:

@Transactional(propagation = Propagation.REQUIRED)
public void myTransactionalMethod() {
    
    
    // 事务性操作
}

需要注意的是,事务传播行为仅在方法之间的调用时才会生效,对于同一个方法内部的事务性操作,传播行为不会起作用。

如果将事务比作房子,以伴侣为例子理解(以下图片来自网络):
理解事务传播机制

3.3 嵌套事务(NESTED)和加入事务(REQUIRED )的区别

  1. 整个事务如果全部执行成功,二者的结果是一样的。
  2. 如果事务执行到一半失败了,那么加入事务整个事务会全部回滚;而嵌套事务会局部回滚,不会影响上一个方法中执行的结果。

写在最后

本文被 JavaEE编程之路 收录点击订阅专栏 , 持续更新中。
 以上便是本文的全部内容啦!创作不易,如果你有任何问题,欢迎私信,感谢您的支持!

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_60353039/article/details/131823737