当前位置: 代码迷 >> 综合 >> 数据库的隔离级别与丢失更新、脏读、不可重复读、幻读详解
  详细解决方案

数据库的隔离级别与丢失更新、脏读、不可重复读、幻读详解

热度:89   发布时间:2023-11-24 11:08:51.0

前言

         因为互联网应用时刻面对着高并发的环境,如商品库存,时刻都是多个线程共享的数据,这样就会在多线程的环境中扣减商品库存。对于数据库而言,就会出现多个事务同时访问同一记录的情况,这样会引起数据出现不一致的情况,这便是数据库的丢失更新(Lost Update)问题。在了解丢失更新之前,让我们回顾下有关数据库事务的基本知识。

一、数据库事务的4个基本特征 

            数据库事务具有以下4个基本特征,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration),简称ACID。

1、原子性(Atomicity)

     指事务必须是一个原子的操作序列单元。事务中包含的各项操作在一次执行过程中,只允许出现两种状态之一。

  • 全部执行成功
  • 全部执行失败

     任何一项操作都会导致整个事务的失败,同时其它已经被执行的操作都将被撤销并回滚,只有所有的操作全部成功,整个事务才算是成功完成。

2、一致性(Consistency)

      指事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处以一致性状态。

      比如:如果从A账户转账到B账户,不可能因为A账户扣了钱,而B账户没有加钱。

3、隔离性(Isolation)

     指在并发环境中,并发的事务是互相隔离的,一个事务的执行不能被其它事务干扰。也就是说,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间。一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务是不能互相干扰的。

4、持久性(Duration)

     指事务一旦提交后,数据库中的数据必须被永久的保存下来。即使服务器系统崩溃或服务器宕机等故障。只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态。

二、读写锁等锁机制介绍

1、读锁:也称为共享锁(S锁),用于不更改或不更新数据的操作(只读操作),如 SELECT 语句。

如果事务T对数据A加上共享锁后,则其他事务只能对数据A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。

2、写锁:也称为排他锁(X锁),用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时对同一资源进行多重更新。

如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。

扩展阅读:如何理解互斥锁、条件锁、读写锁以及自旋锁?

3、乐观锁(Optimistic Lock):每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

4、乐观锁(Optimistic Lock):总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

5、行锁:每次操作锁住一行数据(即一行记录)。

缺点:开销大,加锁慢;会出现死锁。                 优点:锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

6、表锁:每次操作锁住整张表。

优点:开销小,加锁快;不会出现死锁。             缺点:锁定粒度大,发生锁冲突的概率最高,并发度最低。

注:什么是死锁和如何解决死锁

三、丢失更新

在多个事务同时操作数据库的情况下,会引发丢失更新的场景。比如,电商有一种商品,在疯狂抢购(如秒杀)中,会出现多个事务同时访问商品库存的场景,这样就会产生丢失更新。一般而言,存在两种类型的丢失更新,接下来让我们从一个例子中了解他们。

假设一种商品的库存数量还有100,每次抢购都只能抢购1件商品,那么在抢购中就可能出现下表中的情况。

时刻 事务1 事务2
T1 初始库存100 初始库存100
T2 扣减库存,余99  
T3   扣减库存,余99
T4   提交事务,库存变为99
T5 回滚事务,库存100  

从上表中可以看到,T5时刻事务1回滚,导致原本库存为99的变为了100,这导致事务2的提交结果丢失了。

类似地,对于这样一个事务回滚另一个事务提交而引发的数据不一致的情况,我们称之为第一类丢失更新。(既事务回滚导致的另一个事务的更新丢失)

对于第一类丢失更新,因为目前大部分数据库已经解决了,所以我们不对此进行深入讨论。接下来,我们来看看什么是第二类丢失更新。

时刻 事务1 事务2
T1 初始库存100 初始库存100
T2 扣减库存,余99  
T3   扣减库存,余99
T4   提交事务,库存变为99
T5 提交事务,库存变为99  

注意T5时刻提交的事务。因为在事务1中,无法感知事务2的操作,这样它就不知道事务2已经修改过了数据,因此它依旧认为只是发生了一笔业务,所以库存变成了99,这导致事务2提交的结果丢失。

像这样,多个事务都提交而引发的丢失更新称为第二类丢失更新。(即事务A覆盖事务B已经提交的数据,造成的丢失更新)

为了克服第二类丢失更新,数据库提出了事务之间的隔离级别的概念,下面,让我们详细看看事务的隔离级别。

四、事务的隔离级别

1、未提交读(READ_UNCOMMITTED)

未提交读是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。这是一种危险的隔离级别,所以一般在我们实际的开发中应用不广,但是它的优点在于并发能力高,适用于那些对数据一致性没有要求而追求高并发的场景,他的最大坏处是出现脏读

时刻 事务1 事务2 备注
T0     商品库存初始为2
T1 读取库存为2    
T2 扣减库存   此时库存为1
T3   扣减库存 此时库存为0,因为读取到事务1未提交的库存数据
T4   提交事务 库存保存为0
T5 回滚事务   因为第一类丢失更新已经解决,所以不会回滚为2。因此此时库存为0

上表的T3时刻,因为采用未提交读,所以事务2可以读取事务1未提交的库存数据(库存为1),这里当它扣减库存后提交,数据库将保存此数据为0。当事务1回滚时,结果仍然为0。很显然,这是错误的,发生了脏读现象。

脏读:A事务读取B事务尚未提交的更改数据,并在这个数据的基础上进行操作,这时候如果事务B回滚,那么A事务读到的数据是不被承认的。

为了克服脏读的问题,数据库隔离级别提供了读写提交(READ COMMITTED)

2、读写提交(READ COMMITTED)

读写提交隔离级别,是指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。

相当于加了个读锁,当我在读取数据时候,其他事物只能读取,不能修改(数据库中的)数据。)=》这样形容有点不大准确。

时刻 事务1 事务2 备注
T0     商品库存初始为2
T1 读取库存为2    
T2 扣减库存   此时库存为1
T3   扣减库存 库存为1,因为是读写提交隔离级别,读取不到事务1未提交的库存数据
T4   提交事务 库存保存为1
T5 回滚事务   因为第一类丢失更新已经解决,所以不会回滚为2。因此此时库存为1,结果正确

在T3时刻,由于采用了读写提交的隔离级别,所以事务2不能读取到事务1中未提交的数据,因此解决了脏读问题。但是,读写提交也会导致下面的问题:

时刻 事务1 事务2 备注
T0     商品库存初始为1
T1 读取库存为1    
T2 扣减库存   事务未提交
T3   读取库存为1 认为可扣减
T4 提交事务   库存保存为0
T5   扣减库存 失败,因为此时库存为0,无法扣减

在T3时刻事务2读取库存时候,因为事务1未提交事务,所以读出的库存为1,于是事务2认为当前可扣减库存。当时当T4时刻,事务1提交事务后,在T5时刻,事务2会发现扣减失败。像这样的问题,叫做不可重复读

不可重复读是指A事务读取到了B事务已经提交的更改数据,在同个时间段内,两次查询的结果不一致。

为了克服这个不足,数据库的隔离级别进一步提出了可重复读的隔离级别。

3、可重复读(REPEATABLE READ)

可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读写提交的时候,可能一些值的变化,影响当前事务的执行,如上诉的库存就是一个变化的值。接下来,让我们看看可重复读隔离级别是如何解决不可重复读的。

时刻 事务1 事务2 备注
T0     商品库存初始为1
T1 读取库存为1    
T2 扣减库存   事务未提交
T3   尝试读取库存 不允许读取,等待事务1提交
T4 提交事务   库存保存为0
T5   读取库存 库存为0,无法扣减

可以看到,事务2在T3时刻尝试读取库存,但是此时这个库存已经被事务1事先读取,锁住了(相当于加了个写锁,不允许其他事务读和写,实际上是使用行级锁,只锁住该行数据,不允许其他事务读写),所以这个时候数据库就阻塞了事务2的读取,直至事务1提交,事务2才能读取库存的值。此时,完美解决不可重复读问题。

但是这样也会引发新的问题——幻读

时刻 事务1 事务2 备注
T0 读取库存50件   商品库存初始为100,现在已经销售50件,库存50件
T1   查询交易记录,50笔  
T2 扣减库存   事务未提交
T3 插入一笔交易记录    
T4 提交事务   库存保存为49件,交易记录为51笔
T5   打印交易记录,51笔 这里与查询的不一致,在事务2看来有一笔交易记录是虚幻的

 这便是幻读现象。幻读是指A事务读取B事务提交的新增数据,导致的幻读现象。

注意:幻读和不可重复读的区别

        不可重复读是指读到了已经提交的事务的更改数据(修改或删除),幻读是指读到了其他已经提交事务的新增数据。对于这两种问题解决采用不同的办法。为了防止读到更改数据(解决不可重复读),只需对操作的数据添加行级锁,防止操作中的数据发生变化;而为了防止读到新增数据(解决幻读),往往需要添加表级锁,将整张表锁定,防止新增数据(oracle采用多版本数据的方式实现)。

4、串行化(SERIALIZABLE)

串行化是数据库中最高的隔离级别,它要求所有的事务排队顺序执行,即事务只能一个接一个地处理,不能并发。所以它能完全保证数据的一致性。

隔离级别和可能发生的现象总结如下:

隔离级别 脏读 不可重复读 幻读 第一类丢失更新 第二类丢失更新
未提交读 允许 允许 允许 不允许 允许
读写提交 不允许 允许 允许 不允许 允许
可重复读 不允许 不允许 允许 不允许 不允许
串行化 不允许 不允许 不允许 不允许 不允许

注意:事务的隔离级别和数据库并发性是成反比的,隔离级别越高,并发性越低。所以应该根据实际情况选择事务的隔离级别。

对于隔离级别,不同的数据库对其的支持也是不一样的。例如,Oracle只能支持读写提交和串行化,而MySQL则能够全部支持。对于Oracle默认的隔离级别为读写提交,MySQL则是可重复读。

 

 

 

  相关解决方案