一般的数据库教科书上都会介绍,事务有ACID四个特性,分别是atomicity, consistency, isolation和duriablity。本文主要讨论是事务的isolation特性,即隔离性。
谈到事务的隔离性,一般是指ANSI SQL标准下的四种隔离级别,即Read Uncommitted, Read Committed, Repeatable Read和Serialibility。但ANSI SQL的事务隔离级别的标准存在以下限制:
本文主要在分析ANSI SQL标准下的事务隔离级别之后,讨论其限制,以及扩展,全文的组织架构如下:
本文收录在 papers项目 ,papers项目旨在学习和总结分布式系统相关的论文。
在数据库中,多个事务往往是并发执行的,事务之间可能会存在干扰,从而导致数据不正确的问题。为了保证事务之间执行不互相干扰,最简单的方案则是串行的执行一个个事务,但这会降低吞吐量。为此,ANSI SQL标准引入事务隔离级别,描述了并发事务的各种干扰级别,使得应用程序可以在吞吐量和正确性上做决策,不同的事务隔离级别保证不同程度的正确性,一般而言,事务隔离级别越低,吞吐量越高,正确性越低。
ANSI SQL的隔离级别主要是从解决应用程序出现的各种干扰现象中,而设计出来的,其隔离级别主要是为了解决以下三种现象:
首先,来看脏读P1的发生的时序:
事务T1写了数据X,事务T2读了数据X,事务T1回滚,此时,事务T2读取到的是脏数据
其次,来看不可重复读P2发生的时序:
事务T1读了数据X,事务T2写了数据X,事务T2提交,事务T1再次读数据X,两次读到的数据X不一样
最后,来看幻读P3发生的时序
事务T1读了满足X>=m且X<=n的数据,事务T2插入一条数据,满足条件X>=m且x<=n,事务T2提交,事务T1再次读满足X>=m且X<=n的数据,两次读到的数据不一样
用稍微形式化的语言描述上述现象发生的时序,假设W1(X)表示事务T1修改了数据项X,而R2(X),表示事务T2读了数据项X;W1(P)表示事务T1修改了满足谓词条件P的数据项,而R2(P),表示事务T2读了满足谓词条件P的数据项。C1表示事务T1提交,A1表示事务T1回滚。
脏读P1
W1(X)…R2(X)…A1…R2(X)
不可重复读P2
R1(X)…W2(X)…C2…R1(X)
幻读P3
R1(P)…W2(P)…C2…R1(P)
针对三种现象,ANSI SQL标准设定了四种事务隔离级别,如下:
整个事务个隔离级别,与杜绝的现象的对应关系如下图:
值得一提的是,在Serializable下,不可能发生P1,P2和P3,但并不表明,不发生P1,P2和P3就一定是在Serializable。真正的在Serializable是指事务并发执行下得到的结果,与各个事务串行执行下的某个结果相同。
如上文提到,ANSI SQL有如下限制:
对于第一点,没有提及写操作的隔离性,有如下现象
脏写P0
事务T1修改X,事务T2修改X,事务T1回滚,此时不知回滚到什么值
举个例子说明,假设事务T1修改X前,X=100,事务T1要把修改成10,事务T2要把X修改成20,如下:
X=100 W1X(10) W2X(20) A1
事务T1回滚时,如果选择回滚到X=20,那么如果事务T2再回滚时,无法回滚到最原先的100;如果回滚到X=100,那么事务T2提交时,就无法知道写入的是X=20,在这种场景下,是无解的。
因此,对于写一定是要隔离的,否则,无法保证事务的正确性。所以,对于所有的ANSI SQL标准下的隔离级别,需要增强到都满足P0不发生的级别。
对于写操作,还存在写丢失问题,发生在如下场景
写丢失P4
事务T1读数据X,事务T2读数据X,事务T2修改数据X,事务T1修改数据X,最终写入的数据是脏数据
举个例子,假如原先X=100,事务T1对X加5,事务T2对X减1,预期的结果应该是100+5-1=104,而如果在写丢失的情况下,则如下
T1 T2 R(X=100) R(X=100) W(X=99) W(X=104)
如上场景,最终写入到数据库时,X=104,这属于数据不一致,是应该杜绝的。
写丢失在现象在ANSI SQL的Read Committed级别可能发生,但是Repeatable Read级别不可能发生。为此,引入Cursor Stability隔离级别,保证不会发生P4。但是,P4不会在Repeatable级别发生,因为P2禁止了事务T1读X后,事务T2写X的场景。因此,Cursor Stability的隔离级别处于Read Committed和Repeatable Read之间。
现有的工业界产品,如MySQL等,大多都会采用MVCC等多版本并发控制来达到某种隔离性,而在ANSI SQL标准中,是没有考虑多版本的,因此,有必要讨论多版本并发控制下的隔离级别与现有的ANSI SQL标准下的隔离级别的不同。
先简单的介绍下一种多版本并发控制的思路,即Basic Time Ordering,其流程如下:
可以看出,步骤4中保证了P4写丢失现象不会在Basic Time Ordering中发生,把Basic Time Ordering达到的隔离级别称为Snapshot级别。由于会读取Start-TimeStamp之前提交的记录,因此,Snapshot肯定是满足Read Committed。
接下来,要讨论的是Snapshot和Repeatable Read之间的关系:
Snapshot读取的是某个时间点前的快照,因此,也不会出现不可重复读的现象,所以,从这个角度来说Snapshot的隔离级别要大于Repeatable Read,但考虑以下场景:
R1(X)…R2(Y)…W1(Y)…W2(X)…C1…C2
这种场景在Repeatable Read级别是被禁止的,因为T2读了数据Y之后,T1修改Y并提交了,会导致不可重复读。而对于Snapshot是可能发生这种场景的,以X+Y要满足条件大于0为例,说明Snapshot下可能违反此约束,如下
两个事务开始之前X=1,Y=2,时间戳为10000 事务T1开始,申请时间戳为10001,R1(X)=1 事务T2开始,申请时间戳为10003,R2(Y)=2 事务T1,W1(Y)=0 事务T2,W2(X)=0 事务T1提交,发现X+Y=1+0,满足约束 事务T2提交,发现X+Y=0+2,满足约束
虽然事务T1和事务T2都认为满足约束,但是两个事务都执行完成后,约束不满足。
因此,在这个现象上,Snapshot的隔离级别要小于Repeatable Read。故,Snapshot的隔离级别既不大于Repeatable Read,也不小于Repeatable Read。
事务隔离级别是比较有意思的话题,现阶段也有一些技术来实现各种隔离级别,接下来的一些文章会讨论实现事务隔离级别的技术,以及工业界产品中使用的技术,如下:
PS:
本博客更新会在第一时间推送到微信公众号,欢迎大家关注。