数据库事务ACID特性以及隔离等级

数据库事务正确执行的4个基本要素是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。

ACID特性
原子性 整个事务中的所有操作,要么全部完成,要么全部不完成,不会停滞在中间某一个环节,假若事务在执行过程中发生了错误,那么将会回滚到事务开始执行前的状态,这个事务就像没有被执行过一样。
一致性 一个事务可以改变封装的状态。事务必须始终保持系统处于一致的状态,不管任何给定的时间内并发的事务有多少。
隔离性 指两个事务之间的隔离程度(稍后会说到)。
持久性 在事务完成以后,该事务对数据库所做的更改会持久地保存在数据库中,而且不会被回滚

关于隔离性:

隔离性涉及了多个事务的并发状态,并且隔离性分为几个等级。当多个事务并发状态时,就可能产生数据丢失更新的问题。一般情况可能存在两类丢失更新。

假设某互联网账户存在两种消费形式,刷卡或者互联网消费。一对夫妇同时使用这个账户进行消费,老公使用刷卡消费,老婆使用联网消费,请看如下场景:

场景1:

第一类丢失更新
时刻     事务(1)老公    事务(2)老婆
T1 查询余额10000元 //do nothing
T2 //do nothing 查询余额10000元
T3 //do nothing 网购1000元
T4 请客吃饭1000元 //do nothing
T5 提交事务,余额9000元 //do nothing
T6 //do nothing 取消购买,回滚事务至T2,余额10000元

可以看出,整个事务的过程,老公花掉1000元,而老婆最后却查询剩余10000元,明显不符合事实。此类丢失更新,是由于两个事务,一个提交成功,一个回滚,导致的数据不一致,可以成为第一类丢失更新。(目前大多数数据库已经消灭了这种问题),下面来看另一个场景。

场景2:

第二类事务丢失更新
时刻     事务(1)老公    事务(2)老婆
T1 查询余额10000元 //do nothing
T2 //do nothing 查询余额10000元
T3 //do nothing 网购1000元
T4 请客吃饭1000元 //do nothing
T5 提交事务,消费1000,余额9000元 //do nothing
T6 //do nothing 提交成功,消费1000,余额9000元

 可以看出,整个事务过程有两笔消费分别是老公的和老婆的,但由于是在不同的事务中,事务之间无法互相干涉,所以最后查询出的余额都是9000元,不符合实际情况。这就是第二类丢失更新问题。

因此,为了克服第二类丢失更新带来的问题,也就是事务之间的协助的一致性,数据库标准规范了事务之间的隔离等级,可以在一定程度上减少出现丢失更新带来的问题。

隔离等级:

隔离等级可以在不同程度上减少丢失更新,隔离等级分为4层,分别是:脏读(dirty read)读/写提交(read commit)可重复读写(repeatable read)序列化(serilizable)。下面分别解释这四个事务隔离等级。

1.脏读:脏读是最低的隔离级别,它允许一个事务去读取另一个事务中未提交的数据,参考下列以脏读为隔离级别的例子

脏读
时刻     事务(1)老公    事务(2)老婆 备注
T1 查询余额10000元 //do nothing XXX
T2 //do nothing 查询余额10000元 XXX
T3 //do nothing 网购1000元,余额9000 XXX
T4 请客吃饭1000元,余额8000 //do nothing 事务1读取到事务2,余额为9000,再消费1000,余额为8000.
T5 提交事务,消费1000,余额8000元 //do nothing 余额依旧为8000
T6 //do nothing 取消购买,回滚事务 此时回滚,余额为8000

可以清楚地看到, 其中只有1笔成功的消费,但是最后得到的结果是8000,其原因就是因为脏读,在T4时刻,事务1读取了事务2的余额,而在T6时刻,由于老婆回滚事务(类似第一类丢失更新,但是目前已经不存在这种问题,因此,此时的余额为8000)。

2.读写提交:针对以上脏读所带来的的一些问题,,因此SQL提出了第二个隔离级别,也就是读/写提交。读写提交就是一个事务只能读取另一个事务已经提交的数据。例子:

读/写提交
时刻     事务(1)老公    事务(2)老婆 备注
T1 查询余额10000元 //do nothing XXX
T2 //do nothing 查询余额10000元 XXX
T3 //do nothing 网购1000元,余额9000 XXX
T4 请客吃饭1000元,余额9000 //do nothing 事务2的余额还未提交,不能读出,因此余额为9000
T5 提交事务,消费1000,余额9000元 //do nothing 余额依旧为9000
T6 //do nothing 取消购买,回滚事务 此时回滚,余额为9000

可以看出,此例的事务采用读写提交的隔离级别,一共花费1笔,最后余额为9000,在T4时刻,由于事务2还未提交,所以事务1无法读取到事务2的余额。事务1在T5时刻提交,事务2在T6时刻回滚(并不会带来第一类丢失更新问题,上面已经讲到过)。所以余额9000正确。但是也可能带来另外一种问题,如下:

不可重复读
时刻     事务(1)老公    事务(2)老婆 备注
T1 查询余额10000元 //do nothing XXX
T2 //do nothing 查询余额10000元 XXX
T3 //do nothing 网购1000元,余额9000 XXX
T4 请客吃饭2000元,余额8000 //do nothing 事务2的余额还未提交,不能读出,因此余额为10000-2000=8000
T5 //do nothing 继续网购8000,余额1000 余额依旧为9000
T6 //do nothing 提交订单,提交事务 老婆提交事务,余额为更新为1000
T7 提交事务发现,余额为1000,不足买单 //do nothing 事务1可以读取到事务2已经提交的数据,并发现余额为1000

 可以看出,由于读写提交的隔离级别,在T6时刻事务2提交事务,余额更新为1000,但是事务1在T6时刻前不能读取事务2的数据,所以,从事务1的这一列来看,假若自己是老公,查到余额有10000元,于是和朋友去吃饭,结账时被告知花了2000元,但是却被提示余额不足!其实问题就在于他的账户余额的不能重复读取导致的问题,这种场景就叫做不可重(复)读。

3.可重复读:为了克服上面类似的不可重复读带来的问题,于是SQL标准提出了可重复读的隔离级别来解决问题。可重复读是针对数据库同一条记录而言,会使数据库同一条记录按照一个序列化进行操作。不会产生交叉的情况,因此可以保证数据的一致性。但是,在很多场景,数据库需要同时对多条记录进行读写操作,此时会产生下列情况:

幻读
时刻     事务(1)老公    事务(2)老婆 备注
T1 //do nothing 查询消费记录:10条,准备打印 XXX
T2 开始消费,增加1条消费记录 //do nothing XXX
T3 提交事务 //do nothing XXX
T4 //do nothing 共打印出11条消费记录 事务2,打印得到11条结果,比查询时多了1条,她会思考这一条信息的真实性,这样的场景称为幻读

 在T2-T3时刻,事务1开启一笔消费,并提交,对于事务2,查询到10条结果,但她并不知道事务1的操作,并得到11条记录,于是便质疑多出的一条记录是否多余。产生幻读。

4.序列化:为了克服幻读,SQL标准提出了序列化隔离级别,它是一种让SQL按照顺序读写的方式,能够消除数据库事务之间产生的数据不一致的问题。

最后,各类隔离级别以及可能产生的现象如下表:

      隔离级别 / 现象

脏读 不可重读 幻读
脏读
读/写提交 ×
可重复读 × ×
序列化 × × ×

一般而言,从脏读到序列化的隔离级别,系统性能直线下降,因此级别越高,会严重的抑制并发,导致大量线程被挂起,需要大量的时间恢复,一般建议使用读写提交的方式来设置事务,这样不仅有助于高并发,也抑制了脏读的产生。

原创文章 42 获赞 72 访问量 8208

猜你喜欢

转载自blog.csdn.net/weixin_43249548/article/details/102797440