A Critique of ANSI SQL Isolation Levels
NOTE 本篇摘自论文,理论性很强,本人英语水平比较低,想要对事务隔离做深入了解的人阅读原文更好。
不同的事务隔离级别可以支持不同程度的事务并发度。
ANSI/ISO SQL-92定义了四种隔离级别:
NOTE 这里与MySQL等数据库中的隔离级别名字相同,但是含义略有不同。这是篇针对隔离级别的纯理论文章。
这些级别通过经典的可串行化定义,加上三个禁止的子序列,称为"现象"(phenomena):
ANSI文档中没有明确定义phenomena的概念,但是解释说这些phenomena会导致异常(anomalous)。
ANSI的隔离级别与锁相关。
通过phenomena或anomalies定义隔离级别对实现不基于锁的实现有指导意义。
NOTE 意思是说最开始很多数据库都是通过锁的方式实现的事务隔离。但是为了尽量的提高并发,减少锁的使用可能会有帮助,这篇文章尝试使用不加锁的一些概念,与加锁实现的隔离概念,作对比。达到理论上的不加锁描述上的与加锁实现的一致性。
这篇论文介绍了一些anomaly方式定义隔离级别的缺点。ANSI给出的三个phenomena比较模糊。甚至有些anomalous都没有排除。
NOTE ANSI SQL定义的是某些隔离级别下不会发生某些异象(anomaly)。
事务
一个事务包含了一系列动作,将数据库从一个一致性状态转换到另一个。
历史模型
交叉执行的一系列事务的动作线性化。
冲突
两个动作在不同的事务中执行,操作了同一个数据项,最少一个是写动作,那么在某个历史是冲突的。
数据项可以是表、行、或者页等。
冲突动作也可能是满足某个条件(predicate)的数据项集合,比如
select \* from t where age > 1 and age < 8;
这里的age > 1 and age < 8
就称为predicate
,对这个predicate
加锁,就称为predicate lock
。
ANSI SQL定义了三个phenomena:
操作注记:
w
表示写,r
表示读,c
表示提交,a
表示回滚,大P
表示查询条件(predicate),数字表示事务号。
w1[x]表示事务1写数据x。
r1[x]表示事务1读取x。
r1[P]表示事务1根据条件P(predicate)读取数据。
c1 表示事务1提交。
a1 表示事务1回滚。
P1(Dirty Read):
P1: w1[x]...r2[x]...((c1或a1) 和 (c2或a2) 任意顺序)
A1: w1[x]...r2[x]...(a1 and c2 任意顺序)
P1描述了一种现象, 是更为广泛的说明(a broad interpretation),A1描述了P1可能会导致的一种异常(A 指anomaly, a strict interpretation)。
A是P的一个特例。
P2(Non-repeatable Read):
P2: r1[x]...w2[x]...((c1 或 a1) and (c2 或 a2) 任意顺序)
A2: r1[x]...w2[x]...c2...r1[x]...c1
P3(Phantom) 下面的P指predicate,select的条件
P3: r1[P]...w2[y in P]...((c1 或 a1) 和 (c2 或 a2) 任意顺序)
A3: r1[P]...w2[y in P]...c2...r1[P]...c1
NOTE:上面的P1、P2和P3都是在单版本下的数据库(与多版本(single-valued)相对应,MVCC中的MV multi-version)
表1 ANSI SQL隔离级别按照三个原始Phenomena的定义。这不是最终结论。
隔离级别 | P1(或A1) Dirty Read |
P2(或A2) Fuzzy Read |
P3(或A3) Phantom |
---|---|---|---|
ANSI READ UNCOMMITTED | Possible | Possible | Possible |
ANSI READ COMMITTED | Not Possible | Possible | Possible |
ANSI REPEATABLE READ | Not Possible | Not Possible | Possible |
ANOMALY SERIALIZABLE | Not Possible | Not Possible | Not Possible |
ANSI SQL 没有根据这三个phenomena单独定义SERIALIZABLE隔离级别。ANOMALY SERIALIZABLE
指禁止这三种现象(P1P2P3)的隔离级别。
NOTE 从这里看起来好像
ANOMALY SERIALIZABLE
比REPEATABLE READ
级别更强,后面会发现并不一定。
很多SQL产品使用基于锁的方法实现了事务隔离,比如MySQL。
事务在运行过程中,访问某个数据或数据集合时会加读锁或写锁。不同事务对数据加锁会产生冲突,其中一个是写锁。
predicate lock
谓词锁(条件可能更合适,predicate lock): 给定一个搜索条件,对满足这个条件的数据加锁。不仅仅对存在的数据,还包括不存在的数据。
比如select * from t where age > 1 and age < 8;
对age在(1,8)范围加锁,加锁之后,其它的事务不能操作(1
这类似于MySQL InnoDB的gap lock。
well-formed reads/writes
事务在读写一个数据,或者满足某个条件的一些数据时,先加上对应的读写锁,就称为well-formed reads/writes
。
一个事务的读写操作都是well-formed
那么这个事务就是well-formed transaction
。
two-phase locking
2阶段锁:事务访问数据数据时会加锁,不释放锁。在事务结束时(提交或回滚), 释放所有的锁。释放锁之后,不会再加新的锁。
long-duration/short-duration locking
事务持有某个锁,一直到事务提交或回滚,这个锁就称为long-duration
。否则就是short-duration
,就是访问完就释放的锁。
最基础的可串行化理论就是通过well-formed
2阶段锁保证的。
一致性等级 degrees of consistency
为了等价的描述基于锁(locking)、依赖(dependency)和基于异象(anomaly-based),采用分级的方式描述一致性(degrees of consistency)。
表2 一致性等级,通过锁实现的隔离级别
一致性等级 =Locking隔离级别 |
读锁 数据或条件Predicate (不注明时数据项和条件Predicate锁都一样) |
写锁 数据项或条件Predicate (数据项和条件Predicate都一样) |
---|---|---|
等级0 | 不需要 | well-formed writes |
等级1 = Locking READ UNCOMMITTED |
不需要 | well-formed writes long-duration write locks |
等级2 = Locking READ COMMITTED |
well-formed reads short-duration read locks(两个都要) |
well-formed writes long-duration write locks |
Cursor Stability | well-formed reads 当前游标持有读锁 short-duration 读条件(predicate)锁 |
well-formed writes long-duration write locks |
Locking REPEATABLE READ | well-formed read 数据项锁long-duration 条件锁short-duration |
well-formed writes long-duration write locks |
等级3 = Locking SERIALIZABLE |
well-formed reads long-duration read locks(两者都要) |
well-formed writes long-duration write locks |
加locking前缀是为了跟ANSI作对比。
等级0定义了允许脏读脏写的级别。
Locking REPEATABLE READ没有对应的等级。
这个理论是[GLPT]创建的。
Date(应该是一个数据库公司名字)和IBM最开始使用术语"REPEATABLE READ" 表达串行化或者说是Locking SERIALIZABLE
。
ANSI SQL中的REPEATABLE READ
隔离级别没有禁止P3(Phantom)。
Date创建了一个更容易理解的术语Cursor Stability
,对应等级2,可以防止游标更新丢失(后面会讲)。
隔离等级高低
L1 « L2:某个隔离等级L1比另一个隔离等级L2级别弱(L1 is weaker than L2);
L1 == L2: 两个隔离级别是一致的,等价的。
L1 «= L2: L1 « L2 或 L1 == L2(原文中的符号复制不出来)。
L1 »« L2: L1 和 L2不兼容(后面会看到Snapshot Isolation
与Repeatable Read
是不兼容的)。
备注:
Locking READ UNCOMMITTED
« Locking READ COMMITTED
« Locking REPEATABLE READ
« Locking SERIALIZABLE
基于locking协议的隔离级别定义最少跟对应的基于phenomena的隔离级别强度是一样的,[OOBBGM] 中做了证明。
P0(Dirty Writes) 事务T1写了一个数据,另一个事务T2在T1提交或回滚之前也修改这个数据。如果这时T1或T2执行回滚操作,无法解释清楚哪个数据是正确的。
P0: w1[x]…w2[x]…((c1 或 a1) 和 (c2 或 a2) 任意顺序)
脏写(Dirty Writes)是一个很严重的问题,所有的隔离级别都应该包含进来。
考虑一个历史操作:
H1: r1[x=50]w1[x=10]r2[x=10]r2[y=50]c2 r1[y=50]w1[y=90]c1
H1不是串行化的。这个事务将x账户转移40到y账户。但是H1与A1,A2和A3都不冲突(注意A1中只有a1 abort,没有commit,即异象1描述的不包含提交)。
再对比一下P1的描述:
P1: w1[x]...r2[x]...((c1或a1) 和 (c2或a2) 任意顺序)
很明显,H1与P1是冲突的。说明P1要比A1更广泛,更精确。
类似的例子:
H2: r1[x=50]r2[x=50]w2[x=10]r2[y=50]w2[y=90]c2r1[y=90]c1
H2也不是串行化的。没有事务脏读数据,因此P1满足了,也没有数据读了两次,也没有条件读。这里的问题是T1读取y的时候,x已经过时了。如果T1再读一次x,会违背A2,但是并没有这么做,所以A2也不适用这种场景。使用P2替换A2:
P2: r1[x]...w2[x]...((c1 或 a1) and (c2 或 a2) 任意顺序)
再来看A3和H3,P是某个条件:
H3: r1[P] w2[insert y to P] r2[z] w2[z] c2 r1[z] c1
T1执行一个搜索来找活跃的员工。T1就插入满足条件P的一个新的活跃员工,然后更新z。T1再读取活跃员工个数的时候,就会发现一个不一致的数据。这个历史操作明显也不是可串行化的。但是这个跟A3也是不冲突的,因为没有读取两次。在这里P3是更合理的:
P3: r1[P]...w2[y in P]...((c1 或 a1) 和 (c2 或 a2) 任意顺序)
ANSI SQL隔离级别定义的现象是不完整的,还可以有很多异象发生。因此需要定义新的现象(phenomena)来完善。下面会重新定义P1-P3,删除(c2或a2):
P0: w1[x]...w2[x]...(c1 或 a1) (Dirty Write)
P1: w1[x]...r2[x]...(c1 或 a1) (Dirty Read)
P2: r1[x]...w2[x]...(c1 或 a1) (Fuzzy or Non-Repeatable Read)
P3: r1[P]...w2[y in P]...(c1 或 a1) (Phantom)
表3 ANSI SQL隔离级别,基于现象(phenomena)
隔离级别 | P0 Dirty Write | P1 Dirty Read | P2 Fuzzy Read | P3 Phantom |
---|---|---|---|---|
READ UNCOMMITTED | Not Possible | Possible | Possible | Possible |
READ COMMITTED | Not Possible | Not Possible | Possible | Possible |
REPEATABLE READ | Not Possible | Not Possible | Not Possible | Possible |
SERIALIZABLE | Not Possible | Not Possible | Not Possible | Not Possible |
基于锁的(locking)的隔离级别描述与phenomena的描述是一致的。
Cursor Stability是为了防止丢失更新。
P4(Lost Update): 事务T1读取一个数据然后事务T2更新这个数据,T1根据读取的结果更新这个数据然后提交:
P4: r1[x]...w2[x]...w1[x]...c1 (Lost Update)
示例:
H4: r1[x=100] r2[x=100] w2[x=120] c2 w1[x=130] c1
T1最后给x增加了30,他没有看到T2写入的数据。P4可以在READ COMMITTED隔离级别(注意这里的RC级别不要求读的时候加上写锁,跟平时用数据库时不太一样,这里是纯理论讨论)。禁止P2(REPEATABLE READ)可以避免这个问题,因此T2写x是在T1读取x并且在T1提交或回滚之前的。这样说的话,P4应该是在READ COMMITTED和REPEATABLE READ之间。
Cursor Stability隔离级别扩展了READ COMMITTED的锁行为:游标读取过数据之后,要求在游标移动或关闭之前必须要拿着这个读锁。
P4C: rc1[x]...w2[x]...w1[x]...c1 (Lost Update)
rc1指事务1使用游标读。
READ COMMITTED « Cursor Stability « REPEATABLE READ
快照隔离级别:每个事务都从一个事务开始时的快照中(已经提交的数据)读取数据。事务开始的时间戳称为Start-Timestamp。
数据库为每个事务维护一个对应Start-Timestamp的快照。
事务开始时或者在第一次读取数据时获取一个Start-Timestamp。
快照隔离级别的读取操作不会阻塞。事务的写,也会映射到这个快照中。在Start-Timestamp之后的事务更新的数据,对这个事务是不可见的。
快照隔离是一个多版本并发控制的类型,这与上面描述的都不同。上面描述的隔离都是假定在单版本模式下的,快照隔离是基于多版本(multiversion)的。
事务提交的时候,获取一个提交时间戳Commit-Timestamp,这个时间戳比任何当前已经存在的Start-Timestamp或Commit-Timestamp都要大。
事务T1可以成功提交的前提:没有另一个事务T2在[Start-Timestamp, Commit-Timestamp]时间段内写T1写过的数据。否则的话,T1就会回滚。这个策略称为First-committer-wins,能够防止丢失更新(P4 Lost Update)。
T1提交之后,修改的数据就会对Start-Timestamp比T1的Commit-Timestamp大的事务可见。
NOTE 很多数据库的MVCC通过单调递增序号的方式实现Start-Timestamp和Commit-Timestamp。
快照隔离是一个多版本的方法,单版本的历史描述不一定能够恰当的映射过来。上面描述的H1,如果放在快照隔离下,就是这样的:
H1.SI: r1[x0=50] w1[x1=10] r2[x0=50] r2[y0=50] c2
r1[y0=50] w1[y1=90] c1
[OOBBGM]中证明了所有快照隔离的历史操作都可以映射到单版本的历史操作(保留操作依赖)。多版本的H1.SI映射到单版本:
H1.SI.SV: r1[x=50] r1[y=50] r2[x=50] r2[y=50] c2
w1[x=10] w1[y=90] c1
将多版本的历史操作映射到单版本历史操作是唯一将快照隔离放在隔离架构中对比的严谨的检验方法。
快照隔离不是可串行化的。示例:
H5: r1[x=50] r1[y=50] r2[x=50] r2[y=50] w1[y=-40]
w2[x=-40] c1 c2
H5可以发生在快照隔离级别下。假设修改数据的事务,都有一个约束x + y > 0
。这样的话T1和T2在快照隔离级别下都可以正常的运行。
约束冲突(Constraint violation)是一个更普遍更重要的并发异常。
A5(数据约束冲突 Data Item Constraint Violation)。假设C()
是数据库关于x和y的约束,有两个约束冲突:
A5A Read Skew
A5A Read Skew(偏移,误用): T1读x,然后T2更新x和y为一个新值提交。T1再读取y,会看到不一致的状态。
A5A: r1[x]...w2[x]...w2[y]...c2...r1[y]...(c1 or a1) (Read Skew)
NOTE 事务1读取x和y中间xy被事务2修改了。
A5B Write Skew
A5B Write Skew: T1读取x和y,满足约束条件C()
,然后T2读取x和y,写x再提交。然后T1写y。如果x和y之间有约束的话,可能就会冲突。
A5B: r1[x]...r2[y]...w1[y]...w2[x]...(c1 and c2 occur) (Write Skew)
NOTE 事务1读x写y,事务2读y写x。
如果P2(Non-Repeatable Read or Fuzzy Read)被禁止,那么A5A和A5B都不会发生,因为在T2写数据之前,T1已经读取数据了(按照locking中的描述,需要long-duration read锁)。这样的话,A5A和A5B只是在REPEATABLE READ级别和以下会发生。
READ COMMITTED « Snapshot Isolation
很明显,Snapshot Isolation(快照隔离)不会读取未提交的数据,所以比READ COMMITTED要stronger。
A2(Non-Repeatable Read的Anomaly描述)在快照隔离级别下是不可能发生的,因为事务读取数据时,即使其他事务修改了这条数据,再次读取也会得到相同的数据。
A5B在快照隔离级别下是可能发生的,但是REPEATABLE READ隔离级别下A5B不会发生(前面介绍过)。
快照隔离不会出现A3(Phantom), 就是幻读,因为一个事务读多次都是从快照中读取,所以不会出现幻读。而REPEATABLE READ会出现A3。所以有下面的结论:
REPEATABLE READ »« Snapshot Isolation
使用locking方式实现隔离级别时,相对于更加宽泛的定义,比如多版本的SI,有个区别是,locking是悲观的,SI是乐观的
快照隔离没有排除P3
约束:一系列工作时间总和不能超过8小时。
T1通过这个条件读取数据,发现时间总和是7小时,就增加了1个小时。
另一个并发的事务T2做同样的事情。
因为两个事务修改的是不同的数据,因此不会因为First-committer-wins而中断。
所以这个场景在快照隔离下是可以发生的。
快照隔离不会出现A3,不会有幻读,这个很明显。
快照隔离不会出现A1、A2和A3,这样就有:
ANOMALY SERIALIZABLE « SNAPSHOT ISOLATION.
NOTE 这里的ANOMALY SERIALIZABLE是指基于Anomaly来定义的,不是基于Phenomaly定义的。
这里的意思是ANOMALY SERIALIZABLE不一定比REPEATABLE READ更强?
通过这个描述,知道ANOMALY SERIALIZABLE不比REPEATABLE READ级别高,跟我们平时遇到的MySQL中的定义是不同的。
有些系统快照隔离级别仅支持只读事务。
Oracle提供了Read Consistency隔离,每条SQL语句都可以读取到语句开始时时间戳对应的最近的已经提交的数据。行修改操作需要加锁,这样就不是First-committer-wins,而是first-writer-wins。
这篇文章尝试对ANSI SQL定义的隔离级别做一个审视,发现其中有些描述是不完整的。Dirty Writes(P0)没有被排除掉。
ANSI SQL想要将REPEATABLE READ定义为排除所有的异象(anomaly),除了幻读(phantom)。但是基于anomaly描述的表1并没有达到这个效果,不过基于locking的定义可以做到。
ANSI选择的Repeatable Read术语非常不幸:
结合商业上流行的隔离级别,在REPEATABLE READ和SERIALIZABLE之间,表3已经很强大。
Serializable == Degree 3 == {Date,DB2} Repeatable Read
| \
| P3 A5B \ A5B
Repeatable Read ---------- Snapshot Isolation
P2 / | P2 A3 /
/ | /
Oracle / Cursor Stability /
Consistent Read | /
\ | P4C / A3,A5A,P4
\ P4C | /
Read Committed == Degree 2
|
| P1
Read Uncommitted == Degree 1
|
| P0
Degree 0(Dirty Write)
表4 根据可能出现的异象(Anomalies)制定的隔离级别特征
隔离级别 | 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 == Degree 1 |
Not Possible | Possible | Possible | Possible | Possible | Possible | Possible | Possible |
READ COMMITTED ==Degree 2 |
Not Possible | Not Possible | Possible | Possible | Possible | Possible | Possible | Possible |
Cursor Stability | Not Possible | Not Possible | Not Possible | Sometimes Possible | Possible | Possible | Possible | Possible |
REPEATABLE READ | Not Possible | Not Possible | Not Possible | Not Possible | Not Possible | Possible | Not Possible | Not Possible |
Snapshot | Not Possible | Not Possible | Not Possible | Not Possible | Not Possible | Sometimes Possible | Not Possible | Possible |
ANSI SQL SERIALIZABLE ==Degree 3 ==Repeatable Read Date,IBM,Tandem,… |
Not Possible | Not Possible | Not Possible | Not Possible | Not Possible | Not Possible | Not Possible | Not Possible |
NOTE 通篇阅览这篇论文,需要明确一点,就是这里讲的REPEATABLE READ和ANSI SQL SERIALIZABLE/Anomaly SERIALIZABLE,与MySQL等常用数据库产品中描述的级别是不同的。MySQL中介绍的REPEATABLE READ也会加predicate lock,就是gap lock和next-key lock(唯一索引仅锁对应的行)。不过,MySQL中的RR(REPEATABLE READ)级别, 也是会出现幻读(Phantom)(前提是不加锁的读)。MySQL实现隔离级别的方法也是locking策略。
[GLPT] J. Gray, R. Lorie, G. Putzolu and, I. Traiger,“Granularity of Locks and Degrees of Consistency in a Shared Data Base,” in Readings in Database Systems, Second Edition, Chapter 3, Michael Stonebraker, Ed., Morgan Kaufmann 1994(originally published in 1977)
[OOBBGM] P. O’Neil, E. O’Neil, H. Berenson, P. Bernstein, J. Gray, J. Melton, “An Investigation of Transactional Isolation Levels,” UMass/Boston Dept. of Math & C.S. Preprint