事务
综述
事务是数据库提供服务的基本逻辑单元,其满足如下关键特性(ACID):
-
原子性(atomicity ):归属于单个事务的所有动作要么全部被执行,要么全部都不执行;
-
一致性(consistency ):事务的运行不改变数据的一致性,事务能够正确地将目标数据集合从一个一致性状态变换为另一个一致性状态,事务运行过程中数据的状态可能是不一致的,但在执行前和执行后一定是一致的;
-
隔离性(isolation ):任何时刻所见的任何数据集合都是一致的状态,执行中的事务在提交完成之前不能向任何其它事务透露其执行结果;
-
持久性(durability ):一旦事务提交成功,其实施的结果就是永久的,不会受制于之后的系统故障影响;
在前面章节我们知道数据库通过前像和回滚提供事务的原子性,通过后像和前滚解决持久性和性能之间的矛盾。一致性和隔离性主要解决事务并发的正确性和可见性问题,本章重点讨论一致性和隔离性的理论基础,“同步与互斥”章节讨论并发访问的底层设计,“并发控制”章节讨论数据库基于理论基础和底层同步机制设计与实现合适的并发控制算法。
ANSI SQL隔离级别
序列化
事务是一系列动作的集合,并将数据库从一个一致性状态转移到另一个一致性状态。某事务并发控制算法是否正确?有没有一个统一的衡量标准呢?标准要满足普适性,不能关注算法的设计与实现,否则会限制算法实现的多样性。事务并发控制算法的衡量标准是以最终结果的正确性来衡量的。假设所有事务都串行执行,那么执行结果一定是正确的。多个事务按照某种并发控制算法执行的结果和这些事务按照某种串行顺序执行的结果相同,那么该并发控制算法就是正确的,即可序列化是检查并发控制算法正确性的标准。
可序列化是验证并发控制算法的标准。但现实中,某些情况下为了很多更高的并发性能,可以适当放弃部分正确性。为此,ANSI SQL定义了隔离级别,让用户在并发性能和正确性之间进行权衡和选择。隔离级别越高,正确性越高,并发性越低。相反,隔离级别越低,正确性越低,并发性越高。
隔离级别分析
为了最大化地统一一致性和隔离性,ANSI SQL定义了ANSI SQL隔离级别,但ANSI SQL级别有如下缺点:
-
在ANSI 定义隔离级别时,市场上已经有比较成功的商业数据库,这些数据库已经在使用隔离级别中的某些术语,而这些术语的含义和ANSI SQL 定义的术语并不一致;
-
ANSI SQL 设计者希望寻找一种可以被不同实现广泛采纳的定义,而不仅仅是基于锁的实现,所以采用自然语言描述异常的方式来定义隔离级别,带来了很大的模糊性和不准确性;
ANSI SQL为了定义隔离级别首先定义了三种异常:
-
Dirty Read (脏读):事务T1 修改了某条记录,另外一个事务T2 在T1 提交或回滚前读取了该条记录。如果T1 在之后的某个时刻回滚了,T2 读取的数据是永远都不存在的;
-
Non-Repeatable Read 或者Fuzzy Read (不可重复读):事务T1 读取某条记录,之后另外一个事务T2 修改或删除了该记录并完成了提交。此时,如果事务T1 再次读取该记录,将会读到不同值或者发现该记录不存在了;
-
Phantom (幻读):事务T1 根据条件<search condition> 读取记录集合,之后另外一个事务T2 创建了一些满足条件<search condition> 的记录并完成提交。如果事务T1 根据原来的条件<search condition> 再次读取,两次读取的结果集是不同的;
表4.2-1 ANSI SQL定义的四种隔离级别
隔离级别 |
Dirty Read |
Fuzzy Read |
Phantom |
Read Uncommitted |
允许 |
允许 |
允许 |
Read Committed |
不允许 |
允许 |
允许 |
Repeatable Read |
不允许 |
不允许 |
允许 |
Serializable |
不允许 |
不允许 |
不允许 |
定义了三种异常之后,就很容易定义隔离级别了,具体如表4.2-1所示。从表中我们可以看出隔离级别越高允许的异常越少,所以在隔离级别强度方面Read Uncommitted << Read Committed << Repeatable Read << Serializable。ANSI SQL是基于自然语言定义的,有非常大的模糊性。为此,Hal Berenson采用形式化方法进行更加精准的定义。在进行形式化定义前,先了解一下形式化定义的几个基本要素:
-
w1[x] 表示事务1 对记录x 进行写操作,r2[x] 表示事务2 对记录x 进行读操作;
-
事务1 读和写了满足断言P 的多条记录,可以分别用r1[P] 和w1[P] 表示;
-
事务1 的提交或退出(回滚)可以分别用c1 和a1 表示;
-
H (history )由w 、r 、c 、a 组成,表示多个事务并发执行时的某个具体的执行序列;
有了上述基本的形式化要素之后,我们就可以将ANSI SQL定义的三种异常定义如下:
-
A1 :w1[x]...r2[x]...( 任意顺序的a1 和c2) ;
-
A2 :r1[x]...w2[x]...c2...r1[x]...c1 ;
-
A3 :r1[P]...w2[y in P]...c2...r1[P]...c1 ;
自然语言是模糊的,很容易引起歧义。例如Dirty Read并没有坚持事务T1一定要退出,只是说如果发生了就会产生异常,所以有人将A1解释为:
-
P1 :w1[x]...r2[x]...( 任意顺序的(c1 或者a1) 和(c2 或者a2)) ;
P1意味着不允许任何这样的H:T1修改了记录x,T2在T1提交或退出前读取该记录。P1并没有将自己限定在T1退出或者T2提交,所以在解释范围上P1比A1更广,P1禁止了T1和T2关于提交和退出的所有四种组合,而A1只禁止了两种。P1阻止了将来可能产生异常的所有执行序列,即P1定义了将来可能发生的异常,而A1定义了确实存在的异常。我们将P1称为宽泛解释,A1称为严格解释。实际上,A2和A3也存在宽泛解释:
-
P2 :r1[x]...w2[x]...( 任意顺序的(c1 或者a1) 和(c2 或者a2)) ;
-
P3 :r1[P]...w2[y in P]...r1[P]...( 任意顺序的(c1 或者a1) 和(c2 或者a2)) ;
下面来看ANSI SQL定义的三种异常,采用A1、A2、A3解释合理还是采用P1、P2、P3解释合理。考虑如下历史H1,x和y两个账户之间转账$40:
-
H1 :r1[x=50]w1[x=10]r2[x=10]r2[y=50]c2r1[y=50]w1[y=90]c1
H1是不可序列化的,x和y原来的账户总额为100,从x账户向y账户转账40,x和y的账户总额应该仍然为100,但T2读到的总额为60,是不一致的。H1不属于A1、A2、A3中任何一种。对于A1来说需要其中的一个事务退出,对于A2来说相同的记录需要被同一个事务读取两次,对于A3来说需要一个幻读值,而这些事情都没有在H1中发生。
在对H1进行宽泛解释验证时,发现H1是属于P1的。因此,我们需要对ANSI SQL定义的异常采用P1解释,而不是A1,宽泛解释是正确的。劣势的讨论显示ANSI也应该采用P2而不是A2。区分P2和A2的历史:
-
H2 :r1[x=50]r2[x=50]w2[x=10]r2[y=50]w2[y=90]c2r1[y=90]c1
H2也不是可序列化的,T1读到的总额为140。这次两个事务都没有读到脏数据(未提交数据),因此P1是满足的。数据也没有被读取两次,条件也没有发生变化。H2的问题是在T1读取y时,之前读取的x已经过期了。如果T2再次读取x就会发现x发生了变化,但T2没有再次读取x,所以A2不能解释。如果将A2换成P2,上述问题就解决了,r1[x=50]之后w2[x=10]就违反为了P2。最后来看A3和P3,对于如下历史:
-
H3 :r1[P]w2[insert y to P]r2[z]w2[z]c2r1[z]c1
事务T1首先通过条件<search condition>找出活跃的雇员,然后事务T2插入了一名新的活跃雇员,并更新了雇员总数z,事务T1读取活跃雇员总数,发现不一致。这个历史显然也是不可序列化的,然而和A3并不冲突,因为断言并没有读取两次。不过采用P3解释就解决了该问题。如果P3被禁止,H3就是非法的,这显然是ANSI想达到的目的。
表4.2-2 ANSI SQL四种隔离级别的严格定义
隔离级别 |
Dirty Read(P1) |
Fuzzy Read(P2) |
Phantom(P3) |
Read Uncommitted |
允许 |
允许 |
允许 |
Read Committed |
不允许 |
允许 |
允许 |
Repeatable Read |
不允许 |
不允许 |
允许 |
Serializable |
不允许 |
不允许 |
不允许 |
至此,我们可以得出结论:A1、A2和A3这类严格解释有意想不到的弱点,正确的解释应该是宽泛解释,ANSI的真实意图应该是P1、P2和P3,因此将表4.2-1转换为表4.2-2。
隔离级别拓展
其它异常
ANSI SQL定义了P1、P2、P3三种异常,并通过禁止这三种异常定义了四种隔离级别。ANSI的4.28章节”SQL-Transactions”要求序列化隔离级别必须提供“被大家广泛接受的全序列化执行”能力。ANSI的描述很容易导致一个非常普遍的误解,即禁止了上述三种异常之后就可以得到序列化。实际上,除了P1、P2、P3异常之外,常见的还有如下异常:
-
Dirty Write :事务T1 修改了某条记录,事务T2 在T1 提交或回滚前再次修改了该条记录。如果T1 或者T2 执行回滚操作,无法决策应该回滚到哪个正确的值;
-
Lost Update :事务T1 读取某条记录,然后T2 更新该条记录(可能是基于前面的读),之后T1 再次更新记录(基于更早的读)并提交;
-
Read Skew :假设事务T1 读取记录x 之后,事务T2 将x 和y 更新成新值并提交。T1 再次读取y ,此时看到的x 和y 是不一致的状态(不满足约束C ),因此产生了不一致的输出;
-
Write Skew :假设事务T1 读取x 和y ,此时x 和y 是满足约束C 的,然后事务T2 读取x 和y ,并在修改x 后提交。之后事务T1 修改y ,此时x 和y 之间的约束C 就会被破坏;
我们对上述异常进行形式化定义:
-
P0 (dirty write ):w1[x]...w2[x]...( 任意顺序的(c1 或者a1) 和(c2 或者a2)) ;
-
P4 (lost update ):r1[x]...w2[x]...w1[x]...c1 ;
-
A5A (read skew ):r1[x]...w2[x]...w2[y]...c2...r1[y]...(c1or a1) ;
-
A5B (write skew ):r1[x]...r2[y]...w1[y]...w2[x]...(c1 and c2) ;
Dirty Write破坏了数据的一致性,是不可接受的。假设x和y之间有一个约束(例如x=y),事务T1和T2独立运行时都可以保证该约束的一致性。然而如果两个事务以不同的顺序写x和y就会产生Dirty Write,从而破坏一致性。例如H:w1[x]w2[x]w2[y]c2w1[y]c1,最终结果是T1改了y,T2改了x。如果T1是将x和y都改为1,而T2是都改为2,最终结果是x=2,y=1,不满足x=y这个约束。
Dirty Write的另外一个问题是事务的回滚机制对其有依赖。如果没有对P0进行保护,系统无法通过undo恢复到前像。考虑如下H:w1[x]w2[x]a1,你并不期望回滚到w1[x]之前的前像,因为那样会将w2的更新也清理掉。但如果不恢复到前像,后继事务T2再次退出时,就无法通过回滚w2[x]回到T1和T2之前的前像。
因此,需要修改ANSI SQL以在任何隔离级别下都阻止P0。
Cursor Stability
Cursor Stability隔离级别主要解决Lost Update异常,我们再回滚一下P4(Lost Update)的形式化定义:
-
P4 :r1[x]...w2[x]...w1[x]...c1 ;
P4异常引入的问题是即使事务T2提交了,T2的更新也会丢失。例如对于如下History:
-
H4 :r1[x=100]r2[x=100]w2[x=120]c2w1[x=130]c1
x的最终值仅包含了事务T1增加的30,丢了事务T2增加的20。在Read Committed隔离级别下,P4异常是可能存在的,因为H4和P0(已提交事务先写,然后另外一个事务再写)或者P1(先写后读)并不冲突。然而禁止P2也就排除了P4,因为事务T1提交或回滚前,w2[x]在r1[x]之后。因此,P4对区分Read Committed和Repeatable Read之间的隔离级别是有意义的。
Cursor Stability隔离级别拓展了Read Committed级别下SQL游标的锁行为,具体表现为增加一种新的基于游标的读Fetch。并要求锁保持在游标锁在的记录上。锁会一致保持住,直到游标移动到下一条记录或者关闭(很可能是因为提交引起)。Fetch事务可以更新行记录,此时记录上的写锁会一直保持到事务提交(不会随着游标移动而释放)。此概念可以形式化为rc和wc,rc表示游标读,wc表示对游标所在记录进行写操作。rc1[x]wc1[x]执行序列可以排除另外一个事务w2[x]的干涉。此时可以将现象P4重命名为P4C,且可以被Cursor Stability避免掉:
-
P4C(lost update) :rc1[x]...w2[x]...w1[x]...c1
可见在隔离级别的强度上,Read Committed << Cursor Stability << Repeatable Read,为了防止基于游标的读操作产生Lost Update,Cursor Stability在SQL系统中有着广泛的应用。在某些系统中,Read Committed实际上就是更强的Cursor Stability,这也是ANSI SQL标准允许的。
Snapshot Isolation
Snapshot Isolation是一种多版本方法,在任何时刻每条记录都有多个版本,这些版本由活跃的或者已提交的事务创建。事务的读操作必须选择合适的版本。在Snapshot Isolation隔离级别下事务根据启动时间(又称为Start-Timestamp)读取某个提交版本的快照,时间可以是事务第一个读操作之前的任何。只要Start-Timestamp之前的快照数据被正常维护着,Snapshot Isolation下的事务读就永远不会被阻塞。事务自身的写操作(更新、插入、删除)也会反映到快照中,事务自己的后继读操作可以读到自己的写,其它事务在Start-Timestamp之后的写对本事务是不可见的。
当事务T1准备提交事务时,获取一个Commit-Timestamp时间,该Commit-Timestamp比现存值得任何Start-Timestamp和Commit-Timestamp都要大。事务T1能够成功提交的条件是不存在事务T2,T2和T1写了某条相同的记录,且T2的Commit-Timestamp在T1的[Start-Timestamp, Commit-Timestamp]之间。如果不满足提交条件,T1就必须被回退掉。这个特性又称为First-Committer-Wins,从而阻止了Lost Update(P4)。当T1完成提交,其更改的内容对所有Start-Timestamp大于T1的Commit-Timestamp的事务可见。
了解了Snapshot Isolation隔离级别之后,我们来研究其相对于其它隔离级别的强度。首先再回顾一下Read Skew和Write Skew这两种记录间的约束异常:
-
A5A (read skew ):r1[x]...w2[x]...w2[y]...c2...r1[y]...(c1 or a1) ;
-
A5B (write skew ):r1[x]...r2[y]...w1[y]...w2[x]...(c1 and c2) ;
当x=y时,Read Skew就会退化为Fuzzy Read(P2)。事务读取相关的两条不同记录时非常常见的(如引用的完整性)。银行业的记录通常都存在约束,很容易引起Write Skew(A5B),例如银行账户可以是负数,但账户总额必须是非负的,然而此约束在H5中就会被破坏:
-
H5 :r1[x=50]r1[y=50]r2[x=50]r2[y=50]w1[y=-40]w2[x=-40]c1c2
此处我们期望每个事务更新了x或者y之后,仍然能够保持x+y为整数的约束,事务T1和事务T2在隔离级别内都执行了合理的动作,然而整个H5的执行序列破坏了约束。
排除P2之后,不管是A5A还是A5B都不会发生。因为A5A和A5B都涉及这样一个场景:事务T2修改了一条记录,而该记录之前被一个未提交的事务T1读过。因此,异常A5A和A5B只能用于区分Repeatable Read之下的隔离级别。
在Snapshot Isolation隔离级别下,First-Committer-Wins排除了P0(Dirty Write),时间戳机制排除了P1(Dirty Read),因此Snapshot Isolation不弱于Read Committed。A5A在Read Committed下仍然会发生,但在Snapshot Isolation时间戳机制下不会发生。因此,Read Committed << Snapshot Isolation。
在Snapshot Isolation下即使其它事务修改了记录,事务两次读取该记录的值仍然是相同的,所以异常A2不会发生。然而Write Skew(A5B)在Snapshot Isolation下仍然会发生(例如H5)。在单值历史中,我们已经推导出禁止P2也就排除了A5B。因此,Snapshot Isolation会有一些异常(如A5B),这些异常不会出现在Repeatable Read中。
Snapshot Isolation不会出现A3异常,任何事务都不会看到并发事务的更新,因为基于时间戳机制读取的是该时间上历史记录。然后在Repeatable Read隔离级别下会产生A3异常。可见,Snapshot Isolation禁止了A3,但允许A5B,而Repeatable Read则刚好相反。因此,Repeatable Read >><< Snapshot Isolation。
在Snapshot Isolation下带时间戳的事务有着极大的自由度,他可以做时间穿越(查看数据库的某个历史视图),不会阻塞写操作,也不会被写操作阻塞。当然,如果带旧时间戳的事务尝试更新某条记录,而该记录已经被拥有更大时间戳的事务更新了,那么该事务就必须退出。Snapshot Isolation的乐观方法在只读事务中有着非常明显的并发优势,但对于更新事务来说仍然存在争论。尤其是长时间运行的事务和高竞争的短事务存在竞争,长时间运行的事务对于其写的每条记录来说很可能无法在First-Committer-Wins原则下获胜,所以长事务很可能被退出。可以确认的是,如果短事务更新冲突率较低,而长事务是只读事务,那么Snapshot Isolation一定会有非常好的结果。
隔离级别强度
表4.3-1 异常全景图
隔离级别 |
P0 dirty write |
P1 dirty read |
P4C cursor lost update |
P4 lost update |
P2 fuzzy read |
P3 phantom |
A5A read skew |
A5B write skew |
Read Uncommitted |
不可能 |
可能 |
可能 |
可能 |
可能 |
可能 |
可能 |
可能 |
Read Committed |
不可能 |
不可能 |
可能 |
可能 |
可能 |
可能 |
可能 |
可能 |
Cursor Stability |
不可能 |
不可能 |
不可能 |
部分可能 |
部分可能 |
可能 |
可能 |
部分可能 |
Repeatable Read |
不可能 |
不可能 |
不可能 |
不可能 |
不可能 |
可能 |
不可能 |
不可能 |
Snapshot |
不可能 |
不可能 |
不可能 |
不可能 |
不可能 |
部分可能 |
不可能 |
可能 |
Serializable |
不可能 |
不可能 |
不可能 |
不可能 |
不可能 |
不可能 |
不可能 |
不可能 |
图4.3-1 各隔离级别的强弱关系
表4.3-1给出了各隔离级别下异常的出现情况,图4.3-1更加形象地给出了各隔离级别间的强弱关系:
-
Read Uncommitted :强度最弱,仅解决了Dirty Write (P0 )异常;
-
Read Committed :强度大于Read Uncommitted ,在Read Uncommitted 的基础上进一步解决了Dirty Read (P1 )异常;
-
Cursor Stability :强度大于Read Committed ,在Read Committed 的基础上进一步解决了Cursor Lost Update (P4C )异常;
-
Repeatable Read :强度大于Cursor Stability ,在Cursor Stability 的基础上进一步解决了Fuzzy Read (P2 )异常;
-
Snapshot :强度大于Read Committed ,在Read Committed 的基础上进一步解决了Fuzzy Read (P2 )、Phantom (严格解释,A3 )、Read Skew (A5A )、Lost Update (P4 )异常;
-
Serializable :强度最高,在Repeatable Read 基础上解决了Phantom (P3 )异常,在Snapshot 基础上解决了Write Skew (A5B )异常;
-
Repeatable Read 和Snapshot 之间无强弱关系,都有对方解决不了的异常,即Snapshot 有A5B 异常但没有A3 异常,而Repeatable Read 则相反,有A3 异常但没有A5B 异常;
SSI隔离级别
SSI理论
在前面章节我们知道Snapshot Isolation还存在Write Skew异常,所以达不到Serializable隔离级别,这对乐观控制方法来非常致命。因此,SSI理论尝试在Snapshot Isolation基础上找到一种算法可以及时发现潜在的Write Skew异常,并强制部分相关事务退出,打破Write Skew异常,从而达到可序列化目的。
Adya为Snapshot Isolation定义了MVSH图(Multi-Version Serialization History),从而有效识别执行序列是否可序列化。MVSH图中每个节点代表一个事务,节点与节点的边存在三种依赖关系:
-
wr-dependencies :事务T1 生成记录的一个版本,事务T2 读取该版本,且事务T1 在事务T2 前面执行,那么就形成一条从节点T1 指向节点T2 的一条边;
-
ww-dependencies :事务T1 生成记录的一个版本,事务T2 修改该记录,生成一个更新的版本,且事务T1 在事务T2 前面执行,那么就形成一条从节点T1 指向节点T2 的一条边;
-
rw-antidependencies :事务T1 生成记录的一个版本,事务T2 读取之前的版本,且事务T2 在事务T1 前面执行,那么就形成一条从节点T2 指向节点T1 的一条边;
在MVSH图中,如果形成了循环,那么该事务执行序列就是不可序列化的,否则是可序列化的。这个算法理论上很完美,但有一个致命的缺点,就是复杂度比较高,很难用于工业生成环境。在Snapshot Isolation下,wr-dependencies要求T1的写对T2可见,意味着在T2开启前T1必须已经提交,所以已经是串行执行。ww-dependencies一般通过锁解决,或者通过乐观控制解决。所以重点在SI中解决rw-antidependencies引起的异常。
Adya观察发现每个循环至少有2个rw-antidependencies边,Fekete进一步发现这2个边是相邻的,并给出如下结论:
定理:每个循环至少含有T1->T2->T3,这两个边都是rw-antidependencies,且T3必须是循环中第一个提交的事务。
不管是Adya的MVSH还是Fekete的定理,SSI的本质是在并发执行序列中找到满足不可序列化的要素,然后强制某个事务退出,从而打破这些要素的形成,让整个执行序列可序列化。MVSH和定理的区别有:
-
MVSH 需要跟踪rw 、ww 、wr 三种依赖情况,并实时监测是否存在循环;
-
定理只需要跟踪连续的rw ,同时监测提交顺序;
由于定理的跟踪和监测成本远低于MVSH,所以SSI的商业化实现一般都采用定理(如PostgreSQL、Cockroach)。
False Positive
在Fekete的定理中,跟踪是否存在事务T2,既有进来的rw依赖,又有出去的rw依赖。如果存在,这认为当前的执行序列是一个dangerous structure。监测dangerous structure的成本相对较低,而实时监测并比较事务的提交顺序成本相对较高。一些SSI商业实现,一旦发现存在dangerous structure,就强制事务退出,从而预防出现不可序列化的执行序列。然而这样就很可能导致误操作,即实际上可序列化的执行序列也强制退出了,这就是False Positive。Cahill尝试跟踪提交顺序,然而效果并不好。定理是和记录相关的,如果不考虑记录,仅仅进行粗粒度的提交顺序跟踪,仍然会有False Positive。
只读事务优化与延时事务
对于定理中的dangerous structure,如果T1是只读事务,且T3在T1获取snapshot之前提交,那么该dangerous structure就是False Positive,是可以被序列化的。可见只读事务T1会导致不可序列化依赖如下条件(dangerous structure中只有T1可能是只读事务,T2不可能是只读事务):
-
condition1 :有一个从T1 到T2 的rw-antidenpendencies 依赖;
-
condition2 :有一个从T2 到T3 的rw-antidenpendencies 依赖,且T3 在T1 获取snapshot 之前提交;
Condition2是一个非常强的条件,尤其是T3要在T1获取snapshot之前提交。这意味着,如果只读事务T1在运行期间没有并发的读写事务,或者并发的读写事务都已经提交,那么该只读事务T1就是Safe Snapshot,T1不会出现在任何不可序列化的执行序列中,也就不需要在对事务T1做任何跟踪和检查,事务T1也不会被强制退出。
Safe Snapshot可以极大地降低跟踪和监测成本,并能确保只读事务不会被强制退出。这些优点对长时间运行的只读事务来说非常有吸引力,为此设计出延迟事务(Deferrable Transaction),其原理是阻塞只读事务启动直到当前的并发读写事务全部完成。
Locking
大部分商业数据库都是基于锁来实现隔离级别,通过锁可以更好地理解隔离级别与异常的关系。事务在读或者写记录时会申请记录的读(共享)锁或写(排它)锁。如果不同事务的两个锁涉及同一条记录,且至少有一个是写锁,就会产生冲突。如果某个事务持有锁,另外一个事务申请了相冲突的锁,那么新的锁请求不会被赋予直到前面的事务释放该冲突的锁。
读断言锁是在所有满足<search condition>的记录上加锁,写断言锁也一样。断言锁既包括了当前所有满足条件的记录,也包括了即将满足条件的幻想记录(如满足条件的待插入记录,或者更新后满足条件的记录)。如果两个不同事务的断言锁覆盖的数据有交集(包括幻想记录),且至少有一个是写锁,那么这两个断言锁存在冲突。
在写(读)具体某条记录或者满足断言的记录集合前,先在对应的记录或者记录集合上加上写(读)锁或者断言锁,那么这样的读写操作称为well-formed的读写操作。如果事务中的所有读写操作都是well-formed,那么该事务就是well-formed的。
某事务一旦释放过锁之后就不再申请任何新的锁,那么该事务使用的就是两阶段锁。如果事务申请的锁会一直持有直到事务提交或退出,那么这些锁就是长期锁,否则是短期锁。也就是说,短期锁会在动作完成之后立刻被释放。
序列化理论已经证明well-formed的两阶段锁可以保证执行历史可序列化,即满足两阶段锁的任何执行历史都能等价于某个序列化历史。相反,如果事务不是well-formed的或者不是两阶段的,其执行历史很可能不是可序列化的。
表4.5-1 基于锁的隔离级别
隔离级别 |
读锁 |
写锁 |
NULL |
不加锁 |
well-formed writes |
Read Uncommitted |
不加锁 |
well-formed writes 长期写锁 |
Read Committed |
well-formed reads 记录和断言都是短期锁 |
well-formed writes 长期写锁 |
Cursor Stability |
well-formed reads 锁加在当前cursor上 断言是短期锁 |
well-formed writes 长期写锁 |
Repeatable Read |
well-formed reads 记录是长期锁 断言是短期锁 |
well-formed writes 长期写锁 |
Serializable |
well-formed reads 记录和断言都是长期锁 |
well-formed writes 长期写锁 |
如表4.5-1所示,通过锁的类型、范围和生命周期可以严格、准确地定义出隔离级别,但加锁是有成本的,并影响并发性。大部分基于锁的商业数据库在实现上述隔离级别时都做了不同程度的弱化,以获得更好的并发性。
总结与分析
事务的一致性和隔离性主要体现在事务的并发控制算法上,而可序列化是检验并发控制算法正确性的最终标准。如果完全可序列化要求,事务的并发结果没有任何异常,但成本会比较高,事务的并发度一般都比较低,因此需要在并发性能和异常之间进行平衡。如果应用可以接受某些异常,就可以降低算法控制的复杂度和资源等待,显著提高并发性能。
ANSI SQL定义了Dirty Read、Fuzzy Read、Phantom三种异常,并根据这些异常定义出Read Uncommitted、Read Committed、Repeatable Read、Serializable四种隔离级别。整个定义采用基于异常的自然语言描述,存在很大的模糊性和不确定性。优势是具有通用性,不和具体算法绑定,并给厂商留下了很大的发挥空间。缺点是各厂商对Dirty Read、Fuzzy Read、Phantom的理解并不一致,从而导致Read Uncommitted、Read Committed、Repeatable Read等隔离级别下解决的异常并不完全一致。
ANSI SQL的另一个问题是定义的异常不充分,常见的异常还有Dirty Write、Lost Update、Read Skew、Write Skew,根据这些异常可以在ANSI SQL定义的四种隔离级别上进一步定义出Cursor Stability、Snapshot Isolation等隔离级别。
多版本方案读写互不阻塞(MVCC),虽然存在一些异常,但可以极大地提高并发性能,被大部分商业数据库所接受。实际上,大部分商业数据库都综合运用了锁机制和多版本机制,发挥两者的优势。
Snapshot Isolation存在Write Skew异常,不满足可序列化要求。Serializable Snapshot Isolation(SSI)在Snapshot Isolation基础上通过检测冲突关系,并强制部分事务退出,从而达到可序列化,该算法不同于传统的锁算法。传统锁算法是First-Lock-Win,没有得到锁的事务会阻塞等待,而SSI是First-Commit-Win,后提交且不满足可序列化要求的事务会报错回滚。当然SSI会导致一定的误回滚,但该机制是无阻塞的。
表4.6-1 主流数据库的隔离级别
|
Read Uncommitted |
Read Committed |
Cursor Stability |
Repeatable Read |
Snapshot Isolation |
Serializable /SSI |
Oracle |
不支持 |
支持 |
不支持 |
不支持 |
不支持 |
支持 |
MySQL |
支持 |
支持 |
不支持 |
支持 |
不支持 |
支持 |
SQL Server |
支持 |
支持 |
不支持 |
支持 |
支持 |
支持 |
PostgreSQL |
不支持 |
支持 |
不支持 |
支持 |
支持 |
支持(SSI) |
DB2 |
支持 |
不支持 |
支持 |
支持 |
不支持 |
不支持 |
Sybase |
支持 |
支持 |
不支持 |
支持 |
不支持 |
支持 |
表4.6-1给出了主流商业数据库支持的隔离级别,当然有些数据库还支持一些独有的隔离级别(如Oracle的consistent read、DB2的read stability、SQL Server的read committed snapshot等等),并呈现如下特点:
-
ANSI SQL 定义有模糊性,各数据库支持的隔离级别在异常上会有一些细微的差别,只有深入研究个厂商的实现细节才能发现这些差别;
-
MVCC 存在并发优势,被大部分商业数据库所接受,即商业数据库一般都会综合运作锁算法和MVCC ;
-
Repeatable Read 和Snapshot Isolation 解决的异常是不同的,但一些数据库没有区分这两者的不同;
在实现层面,各数据库是有很大差别的,我们将在“并发控制”章节进一步详细讨论主流数据库的并发实现。