07.Spring Boot 事务相关

Spring Boot中的事务管理

事务定义

我们在开发企业应用时,通常业务的一个操作实际上是对数据库读写的多步操作的结合;

由于数据操作在顺序执行的过程中,任何一步操作都有可能发生异常,异常会导致后续操作无法完成,此时由于业务逻辑并未正确的完成,之前成功操作的数据并不可靠,如果要让这个业务正确的执行下去,通常有实现方式:

  1. 记录失败的位置,问题修复之后,从上一次执行失败的位置开始继续执行后面要做的业务逻辑
  2. 在执行失败的时候,回退本次执行的所有过程,让操作恢复到原始状态,带问题修复之后,重新执行原来的业务逻辑

事务就是针对上述方式2的实现。事务,一般是指要做的或所做的事情,或者说一个完整的操作流程。

事务上手

在Spring Boot中,当我们使用了诸如依赖:

  • spring-boot-starter-jdbc
  • spring-boot-starter-data-jpa
  • ``mybatis-spring-boot-starter`;

的时候,框架会自动默认分别注入:

  • DataSource Transaction Manager;
  • Jpa Transaction Manager;
  • Spring Transaction Management;

所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用。

  • 在spring boot的启动类或者某个@Configuration上,加上@EnableTransactionManagent开启事务的支持;

  • 在service中使用@Transactional就可以使用事务了,@Transactional添加在方法中,方法支持事务,如果将@Transactional添加在类上,则整个类的所有方法都默认支持事务;

  • @Transactional默认回滚的是RuntimeExcaption,如果不是抛出的RuntimeException则不会被回滚,(在spring框架下,所有的异常都被org.springwork重写为RuntimeExcetpion);

  • @Transactional(rollbackFor=Excetpion.class),在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚;

  • 使用try catch捕捉异常进行异常处理,除了throw异常,即抛出一场,才会回滚,否则不会触发回滚;

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class Chapter36ApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    @Rollback
    public void test() throws Exception {
        userMapper.insert("AAA", 20);
        userMapper.insert("AAA1", 2);
        userMapper.insert("AAA2", 3);
        userMapper.insert("AAA3", 4);
        userMapper.insert("AAA4", 5);
        userMapper.insert("AAA5", 6);
        userMapper.insert("AAA6", 7);
        User u = userMapper.findByName("AAA");
        Assert.assertEquals(20, u.getAge().intValue());
//        throw new RuntimeException("");
    }

}

上述代码主要通过单元测试演示了如何使用@Transactional注解来声明一个函数需要被事务管理,通常我们单元测试为了保证每个测试之间的数据独立,会使用@Rollback注解让每个单元测试都能在结束时回滚。而真正在开发业务逻辑时,我们通常在service层接口中使用@Transactional来对各个业务逻辑进行事务管理的配置,例如:

public interface UserService {
    
    @Transactional
    User update(String name, String password);
    
}

事务隔离级别

第一种隔离级别:Read uncommitted(读未提交)

如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据,该隔离级别可以通过“排他写锁”,但是不排斥读线程实现。这样就避免了更新丢失,却可能出现脏读,也就是说事务B读取到了事务A未提交的数据

解决了更新丢失,但还是可能会出现脏读

第二种隔离级别:Read committed(读提交)

如果是一个读事务(线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据,该隔离级别避免了脏读,但是可能出现不可重复读。事务A事先读取了数据,事务B紧接着更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

解决了更新丢失和脏读问题

第三种隔离级别:Repeatable read(可重复读取)

可重复读取是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写),这样就可以在同一个事务内两次读到的数据是一样的,因此称为是可重复读隔离级别,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务(包括了读写),这样避免了不可重复读和脏读,但是有时可能会出现幻读。(读取数据的事务)可以通过“共享读镜”和“排他写锁”实现。

解决了更新丢失、脏读、不可重复读、但是还会出现幻读

第四种隔离级别:Serializable(可序化)

提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行,如果仅仅通过“行级锁”是无法实现序列化的,必须通过其他机制保证新插入的数据不会被执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也是最高的,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读

解决了更新丢失、脏读、不可重复读、幻读(虚读)

以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低,像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况来,在MYSQL数据库中默认的隔离级别是Repeatable read(可重复读)。

事务的隔离级别

在MYSQL数据库中,支持上面四种隔离级别,默认的为Repeatable read(可重复读);而在Oracle数据库中,只支持Serializable(串行化)级别和Read committed(读已提交)这两种级别,其中默认的为Read committed级别

指定方法:通过使用isolation属性设置,例如:

@Transactional(isolation = Isolation.DEFAULT)

事务传播行为

所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。

我们可以看org.springframework.transaction.annotation.Propagation枚举类中定义了6个表示传播行为的枚举值:

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}
  • REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  • REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  • NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  • NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED

指定方法:通过使用propagation属性设置,例如:

@Transactional(propagation = Propagation.REQUIRED)

脏读、幻读、不可重复读

脏读

所谓脏读是指一个事务中访问到了另外一个事务未提交的数据,如下图:

img

如果会话 2 更新 age 为 10,但是在 commit 之前,会话 1 希望得到 age,那么会获得的值就是更新前的值。或者如果会话 2 更新了值但是执行了 rollback,而会话 1 拿到的仍是 10。这就是脏读。

幻读

一个事务读取2次,得到的记录条数不一致:

img

上图很明显的表示了这个情况,由于在会话 1 之间插入了一个新的值,所以得到的两次数据就不一样了。

不可重复读

一个事务读取同一条记录2次,得到的结果不一致:

img

由于在读取中间变更了数据,所以会话 1 事务查询期间的得到的结果就不一样了。

posted @   小白酒菊  阅读(51)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
点击右上角即可分享
微信分享提示