01. 数据库中事务的隔离等级及如何设置

一 数据库事务四个特性

数据库的事务指的是一种机制,一系列的操作指令集合,是并发的系统上的最小控制单元

事务把一系列的的命令作为一个整体,一同向数据库进行提交或者回滚,一个事务内的命令要么全部成功,要么全部失败

事务的四个特性 ACID

  1. 原子性Atomicity:一个事务内的所有操作是一个整体,要么全部成功,要么全部失败
  2. 一致性Consistency:当事务完成时,数据库内的数据必须要处于一致的状态;在事务进行过程中,有可能修改了一条记录,但是另外一条记录还没有修改完成,此时就属于不一致的状态。例如,银行的转账,转账的事务完成之后,两个账户里的钱加起来的总数不变
  3. 隔离性Isolation:不同的事务之间的操作互相不能影响。事务必须是独立的,不能依赖于或者影响其他事务,1给2转账,2又给3转账,则两个操作不能同时进行,必须等其中一个事务结束之后才能访问2的数据,否则有可能会出现问题。
  4. 持久性Durability:事务一旦执行完成,那么就保存到了硬盘上,不管系统是否发生故障,数据都不会再发生改变,事务对数据的操作是永久性的

事务的ACID原则保证了事务要么全部执行成功后提交,要么失败全部回滚,使数据恢复到执行前的状态,不会影响数据

二 数据库事务的四个隔离等级以及并发的操作中可能出现的问题

2.1 四种隔离等级及并发操作引起的三种问题

数据库事务有四个隔离等级,从低到高分别是 Read uncommitted、 Read committed、Repeatable read、 Serializable

读未提交、读已提交、可重复读、序列化

不同的隔离等级在并发的环境下可能会出现不同的问题:脏读,不可重复读、幻读

  1. 读未提交:老板发工资(事务A),原本发一万,手抖多打了个0,同时程序员查工资(事务B开启),查到10万开心坏了;结果老板发现输错了就回滚了或者又多打了个0提交了,反正就是程序员看到的工资不对,这个时候事务B读到了A还没有提交的结果,这个就是读未提交,导致了脏读。

    由于读未提交的隔离等级,对于查询的操作是不加锁的,所以这种隔离等级的一致性是最差的,几乎没有人试用这种隔离等级

  2. 读已提交:Oracle,SqlServer的默认隔离等级,程序员去买单(事务A),第一次检测卡里余额还有一万,这时候他老婆转出来了一万(事务B),然后买单事务A要扣款检测的时候发现余额不足了,这就是在A事务中进行两次查询,查到的结果不一样,导致了不可重复读

    读已提交和读未提交相同,在查询的时候都没有加锁,但是读已提交使用了快照读的机制,使得读已提交避免了由于事务B加了写锁导致事务A无法获取读锁而阻塞大大降低性能的问题

    读已提交会导致不可重复读的问题

  3. 可重复读:MySql的默认隔离等级,**程序员去买单开始检查卡里的余额(事务A开启),这个时候他老婆(事务B)就取钱执行更新update的操作了,但是程序员(事务A)在这个过程中看到的余额始终是事务A开启之初创建的视图,在一次事务内看到的一直是事务开启之初的视图,这样子就避免了不可重复读的问题

    程序员开始第一次查败家娘们最近一个月的账单即消费记录(事务A开启),但是这个时候败家娘们(事务B)又产生了一笔消费,在账单上又新增了一笔记录,然后程序员发现第二次查询多了一条记录,这个就是幻读,在A开启了查询的事务,可重复读的隔离等级会禁止事务B更新的操作,但是无法阻止插入或删除的操作,这个就是可重复读,会导致幻读多或者少出来的行叫做幻行

  4. 序列化:最高的隔离等级,在这种情况下,所有的事务都是一个一个排着队等待执行的,这种效率最低性能开销也很大,但是可以避免脏读、不可重复读、幻读

  1. 脏读:假如事务A开始执行更新,同时B也开始执行查询,在A更新之前,B读到了还没有更新的数据,此时就是脏读。由于A还没有提交,B读到的属于脏数据,此时就是脏读
  2. 不可重复读:事务A中要进行两次读取同一条数据的操作,A执行完第一次查询后,B开启事务,开始更新这条数据,导致A第二次读到的数据跟第一次读到的数据不一样,这就是不可重复读。**不可重复读对应的是更新update的操作。**读已提交是只能读到提交后的事务,A的第二次查询必须要等到B更新的事务提交后才能执行
  3. 幻读:事务A要读取两次同一范围内的数据,A读完第一次之后,B往里插入或者删除了几条数据,导致A读到的记录数与第一次不一样,这就导致了幻读。幻读对应的是插入insert或者删除delete的操作,多出来或者少的那些行记录叫做幻行

2.2 快照读

了解快照读

这里指的读,要抛开读写分离的思想,这里的读既包含了select还包含了insert、update等语句中的处理逻辑,读分为两种:当前读和快照读

  1. 当前读:读当前时刻已经提交的数据
  2. 快照读:为数据库创建一个快照,进行读的操作的时候从快照中进行读取

快照读可以理解为在进行读的操作的时候,为数据库创建的一个视图;那么快照是什么时候生成的呢?不同的隔离等级下快照的创建时间是不同的。接下来先复习一下隔离等级:

  1. 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到
  2. 读已提交:一个事务提交之后,它做的变更才会被其他事务看到
  3. 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。未提交的数据对其他事务不可见
  4. 串行化。对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行

那么接下来继续看快照时间的创建时间:

  1. 读未提交下,不创建快照
  2. 读已提交下,在每次sql语句开始执行的时候创建快照
  3. 可重复读下,在事务开启的时候创建快照

2.3 如何知道当前是快照读还是当前读

1. 默认隔离等级下,如果不显示加锁就是快照读

select a from t where id = 1

2. 加锁就是当前读

# 共享锁
select a from t where id = 1 lock in share mode;

#排他锁
select a from t where id = 1 for update;

3. update操作是当前读

update t set a = a + 1;

关于数据库创建快照的更多可以参考网址:https://blog.csdn.net/qq_34679704/article/details/106161807

2.4 总结

  1. 为什么会出现脏读?因为读到了没有提交的数据
  2. 为什么会出现不可重复读?因为创建了多次快照,读到了已经提交的数据
  3. 为什么会出现幻读?insert和delete可以将快照中的记录删除,而且可以不提交就可以影响快照

三 如何设置

3.1 启动类

在启动类上添加@EnableTransationManageMent注解

对于系统需要提供默认事务管理的情况下,实现接口 TransactionManagementConfigurer 指定

为了避免不必要的问题,如果在业务中必须要明确指定 @Transactional 的value的情况下,不建议实现接口 TransactionManagementConfigurer,这样控制台会明确抛出异常,开发人员就不会忘记主动指定

@SpringBootApplication  @EnableTransactionManagement
public class AppMain { 
    public static void main(String[] args) { 
        SpringApplication.run(AppMain.class, args); 
    } 
}

3.2 在Service层类或者方法上使用@Transactional注解

如果没有实现TransactionalManagementConfigurer接口,没有配置默认隔离等级,则必须要在注解中添加Isolation属性配置隔离等级,否则将会抛出异常

@Transactional(isolation = Isolation.DEFAULT,propagation = Propagation.REQUIRED)
public class DefaultFooService implements FooService {
  public void getFoo(Foo foo) {
     // do something
  }
  //方法上注解属性会覆盖类注解上的相同属性
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
  public void updateFoo(Foo foo) {
     // do something
  }
}

3.3 属性配置

3.3.1 隔离等级Isolation

隔离级别是指若干个并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读取、重复读、幻读

我们可以看 org.springframework.transaction.annotation.Isolation 枚举类中定义了五个表示隔离级别的值:

public enum Isolation {  
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}
  1. DEFAULT:默认值,对应的使用数据源的默认的隔离等级,Oracle、SqlServer默认隔离等级是读已提交,会导致不可重复读和幻读的问题;MySql默认隔离等级是可重复读,会导致幻读的问题
  2. READ_UNCOMMITTED:读未提交
  3. READ_COMMITTED:读已提交
  4. REPEATABLE_READ:可重复读
  5. SERIALIZABLE:序列化

3.3.2 七种传播行为

事务的传播行为是针对嵌套事务而言

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

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

3.4 其他属性

  1. value:指定事务管理器的名称
  2. readOnly:是否只读
  3. rollbackFor:指定回滚的异常的类型
  4. rollbackForClassname:指定回滚的异常类名字
  5. noRollbackFor:
  6. noRollbackForClassname:

四 事务的七种传播行为

事务的传播机制主要是针对的嵌套事务而言

@Transactional(propagation = Propagation.REQUIRED)

4.1 REQUIRED

spring默认的事务传播行为就是这个

支持事务,如果方法执行的时候已经在一个事务中,那么加入进去;如果没有事务,那么创建一个事务。

外层事务提交后,内层才会提交

内/外层如果抛出了异常,那么将会一起回滚;只要内层事务抛出了异常,那么就会回滚,无论外层是否有try-catch

因为内外层方法在同一个事务中,内层只要抛出了异常,这个事务就会被设置成rollback-only,即使外层try-catch内层的异常,该事务也会回滚

例子

外层方法在调用内层方法的时候包裹住try-catch,内层方法报错抛出异常。

外层:

	@Override
    @Transactional
    public int addUser(User user) {
        int i = userMapper.insertSelective(user);
        Student student = new Student();
        student.setCourse("cs");
        student.setName("sid");
        try {
            studentService.addStudent(student);
        }catch (Exception e){
            //不抛出
        }
        return  i;
    }

内层:

@Override
@Transactional//(propagation = Propagation.NESTED)
public int addStudent(Student student) {
    int i = studentMapper.insertSelective(student);
    int j =  10/ 0;  // 内层报错抛出异常
    return i;
}

如果内层抛出异常,则尽管外层catch了异常,没有跑出去,但是外层还是会跟着回滚,因为他们在同一个事务中,会一起失败

4.2 SUPPORTS

支持事务,如果当前有事务就加入,没有就算了

如果外层没有事务,就没有事务,不会开启事务;如果外层有事务,那么就加入事务。

如果有事务的话还是会一起提交一起回滚

有事务的话与REQUIRE一样

示例

内层:

	@Override
    @Transactional(propagation = Propagation.SUPPORTS)// 这个addStudent方法的事务传播行为是SUPPORTS。
    public int addStudent(Student student) {
        int i = studentMapper.insertSelective(student);
        return i;
    }

外层:

	@Override
    @Transactional //有事务
    public int addUser(User user) {
        int i = userMapper.insertSelective(user);
        Student student = new Student();
        student.setCourse("cs");
        student.setName("sid");
        studentService.addStudent(student);// 调用addStudent方法,addStudent方法的事务传播机制是SUPPORTS
        return  i;
    }

结果:

如果外层没有事务,则内层被调用之后数据直接就被插入到表里了;如果外层有事务,则要等到外层事务提交后内层才会提交

4.3 MANDATORY

mandatory:强制的

如果存在事务,则加入;否则,如果不存在事务,则抛出异常

实例

内层:

	@Override
    @Transactional(propagation = Propagation.MANDATORY)
    public int addStudent(Student student) {
        int i = studentMapper.insertSelective(student);
        return i;
    }

外层:

	@Override
    @Transactional //有事务
    public int addUser(User user) {
        int i = userMapper.insertSelective(user);
        Student student = new Student();
        student.setCourse("cs");
        student.setName("sid");
        studentService.addStudent(student);// 调用addStudent方法
        return  i;
    }

如果外层有事务,则加入;如果外层没有事务,则抛出IllegalTransactionStateException:No existing transaction found for transaction marked with propagation 'mandatory’

4.4 REQUIRES_NEW

如果外层没有事务,则创建一个事务

如果外层有事务,则创建一个新的事务!内部执行完就提交了,与外部没有关系!

如果内层抛出异常,则内层回滚;外层如果catch了这个异常,则外层不会回滚;如果外层抛出了异常,不会影响到内层

4.5 NOT_SUPPORTED

不支持事务,如果外层有事务,那么内层执行的时候会挂起外层事务,内层执行完毕后,外层事务恢复执行

如果内层抛出了异常,则外层有catch的话不会影响到外层

4.6 NEVER

不支持事务,如果外层有事务,则直接抛出异常IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never’

4.7 NESTED

nested:嵌套的

如果外层有事务,加入事务;如果外层没有事务,则内层开启事务

内层事务要等到外层提交才能提交,如果外层回滚,则内层也会滚,如果内层回滚不影响到外层,则外层正常提交

使用该事务内层回滚不影响外层是有前提的!!!

  1. JDK版本在1.4以上
  2. 事务管理器的nestedTransactionAllowed属性需要设置为true
  3. 外层try-catch内层的异常,因为这样子内层异常就不会影响到内层
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值