【知识库】-数据库_数据库事务与隔离级别
作者:不洗碗工作室 - Hotown
文章出处: 数据库事务与隔离级别
在数据库的使用中,我们常常把一系列操作的集合看作是一个独立的单元,这种构成单一逻辑工作单元的集合被称为事务。
事务模型
一个数据库系统需要维护事务的以下四种特性,它们被合称为ACID,分别对应原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)。
一个简单的事务模型
我们用T来指定这个模型,它的SQL大致是以下形式:
-- 操作1:扣除A账户10元 UPDATE account SET amount = amount - 10 WHERE user_name = 'A' -- 操作2:增加B账户10元 UPDATE account SET amount = amount + 10 WHERE user_name = 'B'
T是一个最简单的转账模型,A用户将10元转到了B用户的账本上。
之后我们就用这个事务模型来解释一下事务的四种特性。
一、原子性
原子性表示,一个事务所包含的操作集合是单一的,不可拆分的。而且事务中任何一个操作失败,都要保证数据库回滚到整个事务执行前的状态。
在T事务中,有两个操作,一个是在A账户扣除10元,一个是在B账户增加10元。这个两个操作密不可分。
如果我们将这两个操作看成两个独立的事务,那么假设初始状态:
A账户:100元
B账户:100元
我们现在执行了操作A,A账户余额变为90元。然后,然后!服务器室由于某些不可控力,发生了爆炸。那么最终结果将会变成:
A账户:90元
B账户:100元
A和B就此开始了无止境的撕逼。。
B:你快给我转钱啊!
A:我转了啊!你看我的账户已经扣了10元了!
B:我这里没有收到啊!你自己看,还是100元!
......
二、一致性
一致性原则要求事务的执行不改变数据库的一致。即事务执行前如果数据库一致,事务执行后,这种一致性仍然存在。
以T事务为例,T执行前,A和B的账户余额总和为200元,那么我们要保证在T执行后,A和B的账户余额总合仍然为200元。
三、持久性
持久性的原则要求一旦事务成功完成执行,并且提交到数据库,那么这一次更新将会持久的。也就是说,只要事务成功执行,任何的系统故障都不能撤销这一次事务的提交。
这个概念可能出现一些小漏洞,比如如果事务的结果储存在内存中,那么一旦宕机,所有的数据都会消失,我们需要将数据提交到外部磁盘上,并且做好更新信息的记录,使数据库在宕机后重启凄然能恢复到之前的状态。由于这个概念不属于我们的讨论范围,这里也就不再赘述。
四、隔离性
隔离性确保事务并发执行后的系统状态与这些事务以某种次序串行执行以后的状态是等价的。
如果有多个事务并发执行,即使我们确保事务的原子性和一致性,这些操作在执行时也并不是严格的串行,而是以某种不可见的形式交叉执行,这种不可见行很可能会导致最终状态的不一致。
举个栗子,我们将之前的事务T记作事务T1,并将T1中的操作细分,他们在系统中的实际操作应该大致是这样的
/** * read(x):从数据库中将x传送到执行read操作的事务的主存缓冲区中 * * write(x):从执行write的事务的主存缓冲区中将x取出并写回数据库(其实还有一个commit过程,这里先忽略) */ read(A); A := A-10; write(A); read(B); B := B+10; write(B);
除此以外,我们再定义一个T2,计算A+B的值:
read(A); read(B); A := A+B;
并行的事务会如何执行呢?如果运气好,它可能会按照T1,T2的顺序完整执行,那么最终我们得到的temp的状态应该是200。
但是如果出现一种情况,当T1中的A扣款成功,并切入数据库,而在执行给B增加余额的操作时,并没有全部完成,而是执行完B := B+10
以后,开始执行T2,虽然B变量确实发生了改变,但是它还没有被写进数据库中,所以T2中计算出的temp变成了90+100=190。
大致流程会是这个样子:
-- T1 read(A): A := A-10; write(A); --这里A在数据库中的值变成了90 read(B); B := B+10; --这里B确实发生了改变,但是并未提交至数据库 -- T2 read(A); --A = 90 read(B); --B = 100(不是110) temp := A+B; --得到190 --T1 write(B) --这里B的修改被提交到数据库
为了确保隔离性,数据库系统中存在一种并发控制系统,来完成这一职责。
事务的隔离级别
在介绍事务的隔离级别前,先来介绍一下脏读,幻读,不可重复读的概念。
脏读、幻读、不可重复读
- 脏读:脏读是指,当一个事务在访问某一数据,并且修改了这一数据,但是在commit之前,另外一个事务也访问了同一数据,然后做了修改。大致模型如下(这里用到了之前忽略的commit操作,这个操作是指事务完成后,进入提交状态):
-- T1 read(A); A := A+1; write(A); --T2 read(A); A := A+2; write(A); commit; -- T1 commit;
- 不可重复读:指在一个事务中,对同一数据进行了两次读取,但是在这个事务还未结束的时候(第一次读取之后,第二次读取之前),另一事务读取了同一数据,并进行了修改,那就导致了两次重复读取的数据不一致,形成了不可重复读状态。
-- 假设初始状态,A=10 -- T1 read(A); -- A = 10; -- T2 read(A); A = A+10; write(A); commit; -- A = 20; -- T1 read(A); -- A = 20,与第一次不同
- 幻读:幻读发生在两个事务非独立执行的情况。下面用SQL演示这种情况:
-- T1 UPDATE users SET status = 1; -- T2 insert users (`status`) values ('0')
然后执行T1操作的用户惊奇的发现,明明把所有的user状态都置1了啊,怎么还有一个0 ??????
事务隔离级别
-
可串行化(Serializable):sql中最高的隔离性级别,能够避免脏读,幻读,不可重复读。代价也相对沉重,会大大影响数据库的性能。
-
可重复读(Repeatable read):只允许读取已提交的数据,而且在一个事务两次读取一个数据项期间,其他事务不得更新该数据。这种状态不能避免幻读。
-
已提交读(Read committed):只允许读取已提交数据,但不要求可重复读。这种状态只能避免脏读。
-
未提交读(Read uncommitted):允许读取未提交的数据。这是最低的隔离级别,脏读,幻读,不可重复读都无法避免。
Attention:所有的隔离级别都不允许脏写,即如果一个数据项已经被另外一个尚未提交或终止的事务写入,则不允许其他事务对它进行写入。