有很多人在学习数据库的事务特性即ACID时对隔离级很糊涂,特别是不能区分脏写(P0)和更新丢失(P4)的异同,本文对这两个异常做一下解释,希望能够对研究数据库事务的人士有所帮助。
P0(Dirty Wirte),即脏写。可以表示为w1[x]...w2[x]... (c1 or a1)
P4(Lost Update),更新丢失。表示为r1[x]...r2[x]w2[x]c2...w1[x]c1
上面表达式的区别为:
P0:在事务T1写入操作w1[x]和提交操作(c1 or a1)之间,被写入了事务T2的数据;P4:在事务读取操作r1[x]和写入操作w1[x]之间写入了另外事务T2的数据。
上面表达式的含义为:
P0(脏写):写入和提交之间,又别写入了别的数据。能带来的后果为两个:
1、不能保证数据的一致性。比如数据库中需要满足x=y,接下来有下面的操作w1[x] w2[x] w2[y] c2 w1[y] c1。T1的操作是x=1,y=1;而T2的操作是x=2,y=2,而上面的结果为x=2,y=1,破坏了数据的一致性。
2、如果T1回滚或T2回滚,不能。假如x初始值为0,T1写入了x=1,T2写入了x=2,此时x的临时结果为2,如果T1回滚,那么x回复为初始值0,那么当T2提交时x仍为0;如果T2回滚,那么也回到初始值为0,接下来当T1提交时,x值仍为0。
P4(更新丢失):其含义为T1要更新x的数据,其首先读取x的值,然后再该值上加1再写回数据库。但是在读取x后,T2写入了新的x并成功提交,而T1还是在老的x值的基础上加1。这样,T2的更新对于T1而言就像被丢弃了一样。
P4提出的目的是让数据自动提供“有状态更新”。例如,事务T1想先读取x当前的数值,如果x为100,则在此基础上把x加20,但如果在这中间有另一个事务T2更改了x的值并提交,那么T1的判断就应该失效了。但是写锁机制下,以及mysql的repeatable read都不能屏蔽这个异常,因为他们只在写操作时才开始上锁。
要注意的是,能够protect from P0的隔离级,不一定能够屏蔽P4。比如最基本的锁系统,为了防止P0的发生会在T1开始写入操作时加锁,这样P0遍不会发生,但是P4因为是读取操作,并不会加锁,T2还是可以写入成功。
微软提出的Snapshot Isolation(SI)是可以屏蔽P4异常的,因为SI在提交时会检查整个事务过程中是否有有别事务写入,如果有则回滚。因此SI不同于传统的写锁机制(只是写操作开始时才上锁)。当然mysql可以使用一些方法来达到类似的效果,例如读取数据的select末尾加上FOR UPDATE。
实际上,写操作可以分为两类,一类为上面所述的我们称之为有状态更新,因为其基于数据原有值的更新;另一类我们称之为无状态更新,有的学者也称之为盲写(BlindWrite)。对于无状态更新的并发写入,我们其实并不应该加锁限制。SI在这方面并没有加以优化。
我带领小组设计并实现的SSCC可以有效的防止上面的异常发生,简单的说,SSCC通过两类不同的写操作来屏蔽P4。感兴趣的话可以访问SSCC的github页面:https://github.com/domino-succ/domino/wiki/%E4%B8%AD%E6%96%87-%E7%AE%97%E6%B3%95%E8%AE%BE%E8%AE%A1,也可以下载代码。有问题请联系我Professor Zhang:zhangtiey@gmail.com