A Critique of ANSI SQL Isolation Levels
摘要
ANSI SQL通过脏读、不可重复读和幻想(如幻读)现象定义了隔离级别。这篇论文介绍了这些现象和ANSI SQL定义未能正确描述的几个比较流行的隔离级别,包括所覆盖级别的标准锁的实现。介绍了更好地描述了隔离级别的新现象。最后,定了一个重要的多版本隔离类型,快照隔离(Snapshot Isolation)。
1 引言
ANSI/ISO SQL-92定义了四种隔离级别:(1)读未提交,(2)读已提交,(3)可重复读,(4)序列化。
2 隔离定义
2.1 序列化概念
一个事务由一组把数据库从一个一致性状态转换到另一个一致性状态的操作组成。一个历史记录将一组事务的交错执行建模为其操作的线性顺序,例如对特定数据项的读写(包括插入、更新和删除)操作。如果不同事务在相同数据项上执行并且其中一个为写操作的两个操作被称为冲突。
2.2 ANIS SQL隔离级别
ANSI-SQL隔离设计人员寻求一个定义,允许许多不同的实现,而不仅仅是锁。他们用以下三种现象来定义隔离:
- P1(脏读):事务T1修改一个数据项。另一个事务T2在T1执行提交或回滚操作之前读取了那个数据项。如果T1执行了回滚操作,那么T2就读取了一个从来都没提交过也就是没有真正存在过的数据项。
- P2(不可重复读 或 模糊读):事务T1读取了一个数据项。另一个事务T2修改或删除了那个数据项并且完成了提交操作。如果T1尝试重新读取这个数据项,它就会收到一个修改过的值或者发现这个数据项已经被修改了。
- P3(幻像/幻读):事务T1在满足特定查询条件读取了一个数据项集合。然后事务T2创建了一个满足T1查询条件的数据项并且完成了提交操作。如果T1在相同的查询条件下重新读取,它会得到一个和第一次读取不同的数据项集合。
论文中操作:w表示写,r表示读,c表示提交,a表示终止回滚,P表示查询条件(predicate)。例如:w1[x]:表示事物1在数据项x上的写操作。
对于同一个隔离级别,P是比A更宽松的解释,P可能会导致异常,A是直接指出了异常。
- 脏读:
P1: w1[x]…r2[x]…((c1 or a1)and(c2 or a2)在任意的顺序)
A1: w1[x]…r2[x]…(a1 and c2在任意的顺序)
- 不可重复读:
P2: r1[x]…w2[x]…((c1 or a1)and(c2 or a2)在任意的顺序)
A2: r1[x]…w2[x]…c2…r1[x]…c1
- 幻像(幻读):
P3: r1[P]…w2[y in P]…((c1 or a1)and(c2 or a2)在任意的顺序)
A2: r1[P]…w2[y in P]…c2…r1[P]…c1
| Isolation | P1(or A1)脏读 | P2(or A2)不可重复读 | P3(or A3)幻象 |
|---|---|---|---|
| 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 在上表中定义了四个隔离级别。每个隔离级别被一个事务禁止出现的现象所定义。但是在ANSI SQL中没有根据这三种现象单独定义SERIALIZABLE隔离级别。ANOMALY SERIALIZABLE是指禁止这三种现象的隔离级别。
从这里看起来好像ANOMALY SERIALIZABLE比REPEATABLE READ级别更强,后面会发现并不一定。
2.3 锁
大部分的SQL产品都是基于锁的隔离。
事务在锁模式下执行请求在它们读和写操作的数据项或数据集上的读(共享)和写(互斥)锁。如果其中至少一个是写锁,那么不同事务在相同数据项上的两个锁就会冲突。
- Read(or write)predicate lock:
读(写)谓词(条件)锁,是指给定一个查询条件,对满足这个查询条件所有数据项上都加锁。
- well-formed writes(reads):
如果一个事务在写(读)那个数据项或被一个谓词定义的数据集之前在每个数据项或谓词上请求一个写锁(读锁),那么这个事务就有well-formed writes(reads)。
如果一个事务拥有well-formed writes和well-formed reads,那么这个事务就是well-formed。
- two-phase writes(reads):
如果一个事务在释放一个写锁(读锁)后没有在数据项上设置新的写锁(读锁),那么这个事务就有two-phase writes(reads)。
- two-phase locking:
如果一个事务在释放一些锁之后没有请求任何新锁,那么这个事务就存在两阶段锁。
- long duration(short duration):
如果一个事务请求的锁一直持有到这个事务完成提交或回滚操作,那么这个锁就是long duration。否则就是short duration。 短锁通常在操作完成之后就会被立刻释放。
表2 一致性等级 通过锁实现的锁隔离级别
| Consistency Level = Locking Isolation Level | Read Locks on Data Items and Predicates(the same unless noted) | Write Locks on Data Items and Predicates(always the same) |
|---|---|---|
| Degree 0 | none required | Well-formed |
| Degree 1 = Locking READ UNCOMMTIED | none required | Well-formed Writes Long duration Write locks |
| Degree 2 = Locking READ COMMITTED | Well-formed Reads Short duration Read locks (both) | Well-formed Writes, Long duration Write locks |
| Cursor Stability | Well-formed Reads Read locks held on current of cursor Short duration Read Predicate locks | Well-formed Writes,Long duration Write locks |
| Locking REPEATABLE READ | Well-formed Reads Long duration data-item Read locks Short duration Read Predicate locks | Well-formed Writes, Long duration Write locks |
| Degree 3 = Locking SERIALIZABLE | Well-formed Reads Long duration Read locks (both) | Well-formed Writes, Long duration Write locks |
定义:
L1 << L2: 隔离级别L1弱于隔离级别L2(或L2强于L1)。
L1 == L2:隔离级别L1和L2是相等的。
L1 =<< L2: L1不强于L2。
L1 >><< L2: 两个隔离级别是不可比较的。
Remark 1:
Locking READ UNCOMMITTED
<< Locking READ COMMITTED
<< Locking REPEATABLE READ
<< Locking SERIALIZABLE
3 ANSI SLQ隔离级别分析
Remark 2
表2中锁方法定义的锁隔离级别至少是和表1中基于现象的隔离级别一样强。
锁隔离级别至少和同名的ANSI级别同隔离度。同样情况下锁隔离级别更强,甚至是在更低的级别。
- P0(脏写):事务T1修改了一个数据项。然后另一个事务T2在T1执行提交或回滚操作之前修改了那个数据项。如果T1或T2之后执行了回滚操作,那么这个原来正确的数值就变得不明确了。
P0: w1[x]…w2[x]…((c1 or a1)and(c2 or a2)在任意的顺序)
Remark 3:
ANSI SQL隔离应修改为所有的隔离级别都需要P0。
历史记录H1:
H1: r1[x=50]w1[x=10]r2[x=10]r2[y=50]c2r1[y=50]w1[y=90]c1
H1不是序列化的。在账户x和y之间转40,保证x和y之和为100。H1并没有和A1、A2和A3冲突。但是H和P1是冲突的。说明宽松解释是更广泛、更正确的。
历史H2:
H2: r1[x=50]r2[x=50]w2[x=10]r2[y=50]w2[y=90]c2r1[y=90]c1
H2是非序列化的。T1读取的账户总额是140。
历史H3:
H3:r1[P] w2[insert y to P] r2[z] w2[z] c2 r1[z] c1
如果P3被禁止,历史H3是不可用的。
Remark 4 严格说明A1,A2和A3有着意想不到的缺点。正确的解释是这些宽松的。我们在下文中假设ANSI是用来定义P1、P2和P3的。
Remark 5. ANSI SQL隔离现象是不完全的。仍然会出现很多异常。必须定义新的现象去完善锁的定义。P3必须被重新表示。在下列的定义中,我们删除对(c2或a2)的引用,这些引用实际上对限制历史没有任何影响。
P0: w1[x]…w2[x]…(c1 or a1) (Dirty Write)
P1: w1[x]…r2[x]…(c1 or a1) (Dirty Read)
P2: r1[x]…w2[x]…(c1 or a1) (Fuzzy or Non-Repeatable Read)
P3: r1[P]…w2[y in P]…(c1 or a1) (Phantom)
表3 在四种现象下解释的ANSI SQL隔离级别
| Isolation | P0 脏写 | P1脏读 | P2不可重复读 | P3幻象 |
|---|---|---|---|---|
| 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 |
Remark 6. 表2和表3的定义是相等的。换句话说,P0,P1,P2和P3是锁行为的重新定义。
4 其他隔离类型
4.1 光标稳定性(Cursor Stability)
光标稳定性是为了阻止更新丢失(Lost update)现象。
- P4(更新丢失):当事务T1读取一个数据项,然后T2更新这个数据项,然后T1(基于前面读到的值)更新这个数据项并且提交时发生。
P4: r1[x]…w2[x]…w1[x]…c1
历史H4:
H4: r1[x=100] r2[x=100] w2[x=120] w1[x=130] c1 c2
H4中T2的更新将会丢失。
Remaek 7:
READ COMMITTED << Cursor Stability<< REPEATABLE READ
4.2 快照隔离(Snapshot Isolation)
使用快照隔离执行的事务总是在事务启动时从这个(已提交的)数据的快照中读取数据。事务在开始或第一次读取数据时会分配一个开始时间戳。快照隔离下事务不会阻塞读操作。事务的写操作(更新、插入、删除)会被映射到一个快照中,事务需要再次获取这个数据时可以再次读取。这个事务开始时间戳之后的其他事务的更新操作对这个事务是不可见的。
- 快照隔离是一种多版本一致性控制。
例子:First-commiter-wins
当事务T1准备提交的时候,它会获得一个提交时间戳,比任何已经存在的开始时间戳和提交时间戳都大。这个事务仅仅当不存在另一个提交时间戳在T1[开始时间戳,提交时间戳]之间的事务T2写数据,T1也写的情况下才能提交成功。否则,T1回滚。
在快照隔离下,同样的操作序列将会变成多版本历史:
H1.SI: r1[x0=50] w1[x1=10] r2[x0=50] r2[y0=50] c2 r1[y0=50] w1[y1=90] c1
多版本历史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
- constraint violation(打破约束):
每个事务在对x和y写一个新值的时候会被期望维持x和y之和为正值的约束。每个事务序列是合法的,但是因为另一个事务正在同时发生改变,结果打破了这个约束。
A5(违反数据项约束):
假设C()是数据库中两个数据项x和y之间的约束。违反约束就会出现两个异常。
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)
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)
-
很明显,A5A和A5B都不会出现在P2被禁止的历史中,因为A5A和A5B都会有一个T2在一个未提交的T1已经读取一个数据项之后修改这个数据项。因此A5A和A5B仅仅能够区分比REPEATABLE READ低的隔离级别
-
Remark 8. READ COMMITTED << Snapshot Isolation
证明:在快照隔离中,first-commiter-wins禁止P0(脏写),时间戳机制禁止P1(脏读),因此快照隔离不弱于读已提交。另外,A5A(写偏移)是可能在读已提交下发生的,但不会再快照隔离下发生。因此得证。
- Remark 9. REPEATABLE READ »« Snapshot Isolation
快照隔离历史禁止异常A3,但是允许A5B,然而REPEATABLE READ是相反的。
- Remark 10. 快照隔离历史禁止A3,因此
ANOMALY SERIALIZABLE « SNAPSHOT ISOLATION
4.3 其他多版本系统
有一些商业化的只读事务快照隔离。
Oracle的读一致性(Read Consistency)隔离在操作开始的时候会给每个SQL最近已提交的数据值。
5 总结和结论
总的来说,传统的ANSI SQL定义的隔离级别有很多问题。定义是模糊不清和不完整的。脏写(P0)没有被禁止。
图2展示了隔离级别之间的关系。表4是通过可能存在的异常定义隔离级别。