乐趣区

关于数据库:云原生分布式数据库事务隔离级别上

Part 1 事务简介

事务的定义

事务(transaction)是数据库系统中保障一致性与执行牢靠计算的根本单位。当确定了查问的执行策略并将其翻译成数据库操作原语后,将以事务为单位执行查问。

辨别数据库 一致性(database consistency)与事务 一致性(transaction consistency)

数据库一致性:如果一个数据库遵从定义于其上的所有一致性(完整性)限度,则数据库处于一致性状态。批改、插入、删除(统称更新)都会造成状态的扭转。现实是保障数据库不会进入不统一状态。尽管事务在执行过程中数据库有可能会临时变得不统一,然而当事务执行结束后数据库必须复原到统一的状态。

事务一致性:波及到并发事务的行为,现实是多个用户同时拜访(读或写)的时候保持一致状态。思考到数据库中数据复制,对用户拜访的解决变得复杂。对于复制数据库,若一个数据项的所有拷贝都具备雷同的值,称这个复制数据库处于互相统一状态(mutually consistency state)。这种状况称为单拷贝等价(one-copy equivalence),在事务都执行完结时所有复制的拷贝都被强制处于同一状态。

事务是一致性与牢靠计算的根本单位。直观上一个事务会通过执行某个操作将数据库从一个版本变成一个新版本,由此造成数据库状态转移。通过事务能够保障如果数据库在执行事务之前是统一的,那么它在执行完事务后仍然是统一的,无论过程中是否有其余事务并行或是产生系统故障。

如果事务胜利地实现了它的工作,称这个事务已提交(commit);如果事务没有实现工作却中途进行了,称它已勾销(abort)。事务会因为多种起因被勾销。此外,死锁等其余起因也会令 DBMS 将事务勾销。当事务被勾销的时候,所有正在执行的动作都会进行,所有曾经执行过的动作都将反做(undo),数据库会回退到执行该事务之前的状态。这一过程被称为回滚(rollback)。

事务的性质

事务 ACID 四个性质:

  1. 原子性(atomicity):事物的所有操作要么全副被执行,要么就一个都不执行,又被称为“All or Nothing”性质。

留神这里把原子性的概念从独自的一个个操作扩大到整个事务了。如果一个事务的执行过程被某种故障所打断,那么事务的原子性就要求 DBMS 可能响应这个故障,并可能决定如何将事务从中复原回来。当然,这里有两种复原形式: 要么实现余下的操作,要么反做所有曾经实现的操作。

个别事务在执行时会遇到两种故障。第一种故障是由输人数据谬误、死锁等起因造成的。在这种状况下,事务要么本人将本人勾销,要么在死锁等状况呈现的时候由 DBMS 将其勾销。在这种故障下保护事务的原子性的操作称为事务复原(transaction recovery)。第二种故障通常源于零碎瘫痪,例如存储介质故障、处理器故障、通信线路损毁、供电中断等。在这种状况下保障事务的原子性的操作称为瘫痪复原(crashrecovery)。上述两种故障的一个重要区别是,在某些零碎瘫痪故障中,存储在易失性存储器中的信息可能会丟失或不可拜访。这两类复原操作属于解决可靠性问题的一部分。

  1. 一致性(consistency): 事务是可能正确的将数据库从一个统一状态变换到另一个统一状态的程序。验证一个事务是否具备一致性是完整性施行所波及到的工作。如何保障事务一致性是并发管制机制的目标。
  2. 隔离性(isolation):在事务提交前,一个执行中的事务不能向其余并发事务走漏本人的执行后果。保障事务隔离性的一个起因在于,爱护事务的一致性。
  3. 持久性(durability):如果一个事务曾经提交,那么它产生的后果是永恒的,这一后果不能从数据库中抹去。持久性会引入数据库复原(database recovery)问题。

这四个性质通常并不是相互独立而是相互依赖的。

事务的类型

这里仅简略介绍几种事务类型:

  1. 立体事务(flat transaction):有一个起始点(Begin_transaction)和一个完结点(End_transaction)。
  2. 嵌套事务(nested transaction):一个事务中蕴含其余具备独自的起始点和提交点的事务。
  3. 工作流(workflow model):理论含意暂没有清晰与对立的定义,目前一个可行定义:为了实现某个商业过程而组织起来的一组工作。

以上次要参考 Principles of Distributed Database Systems (Third Edition)。

Part 2 事务的隔离级别

隔离级别的定义

ANSI(美国国家标准协会)给出的 SQL-92 中隔离级别是依据景象(phenomena)来定义的,上面给出三个景象的解释:

· P1 (Dirty Read): Transaction T1 modifies a data item. Another transaction T2 then reads that data item before T1 performs a COMMIT or ROLLBACK. If T1 then performs a ROLLBACK, T2 has read a data item that was never committed and so never really existed.

· P2 (Non-repeatable or Fuzzy Read): Transaction T1 reads a data item. Another transaction T2 then modifies or deletes that data item and commits. If T1 then attempts to reread the data item, it receives a modified value or discovers that the data item has been deleted.

· P3 (Phantom): Transaction T1 reads a set of data items satisfying some <search condition>. Transaction T2 then creates data items that satisfy T1’s <search condition> and commits. If T1 then repeats its read with the same <search condition>, it gets a set of data items different from the first read.

在论文 A Critique of ANSI SQL Isolation Levels 中作者指出 ANSI 给出的景象是不明确的,即便在最宽松的解释中也不排除执行历史中可能呈现的一些异样行为,会导致一些反直觉的后果。并且,基于锁的隔离级别与等效 ANSI phenomena 有不同的个性,而商业数据库系统通常应用锁实现隔离级别。此外,ANSI 的景象不能辨别出商业系统中许多类风行的隔离级别的行为。

因为 ANSI 给出的景象在语义上存在模糊性,因而能够对景象进行狭义的解释以及广义的解释。

狭义的解释记为 P,广义解释记为 A。事务 1 满足谓词 P 的读取和写入一组记录别离由“r1[P]”和“w1[P]”示意。事务 1 的提交(COMMIT)和停止(ROLLBACK)别离被记为“c1”和“a1”。上述三个景象从新表述如下:

· P1: w1[x]…r2[x]…((c1 or a1) and (c2 or a2) in any order)

· A1: w1[x]…r2[x]…(a1 and c2 in any order)

· P2: r1[x]…w2[x]…((c1 or a1) and (c2 or a2) in any order)

· A2: r1[x]…w2[x]…c2…r1[x]…c1

· P3: r1[P]…w2[y in P]…((c1 or a1) and (c2 or a2) any order)

· A3: r1[P]…w2[y in P]…c2…r1[P]…c1

依据景象给出隔离级别的定义。

ANSI SQL 定义了四个级别的隔离,每个隔离级别的特色是事务中禁止产生的景象(狭义或广义的表述),具体如表 1 所示:

然而,ANSI SQL 标准没有仅依据这些景象来定义可串行化(SERIALIZABLE)隔离级别。ANSI SQL 指出,可串行化隔离级别必须提供“共识的齐全可串行化的执行”。与这个必要条件相比,表 1 导致了一个常见的误会,即不容许三个景象产生就意味着可串行化。不容许在表 1 中呈现的三种景象应该被称为异样可串行化(ANOMALY SERIALIZABLE)(注:异样可串行化意为基于禁止异样(或 phenomena)的可串行化,并非“真正的”可串行化)。

基于锁机制的隔离级别

大多数 SQL 产品都应用基于锁的隔离。因而,只管存在某些问题,但从锁方面表征 ANSI SQL 隔离级别是无效的。

事务在基于锁调度下执行的读 / 写会申请数据项或数据项汇合上读(共享)和写(独占)锁(read lock and write lock)。在两个不同的事务下的锁对应着同一个数据项的状况下,当至多一个是写锁的时候会抵触。

读取(或写入)的谓词锁(给定的 < 搜寻条件 > 确定的一组数据项下)(read/write predicate lock)实际上是对满足 < 搜寻条件 > 的所有数据项的锁。这可能是一个有限集,因为它包含数据库中存在的数据以及以后不在数据库中的所有幻影(phantom)数据项(如果它们被插入,或者以后数据项被更新以满足 < 搜寻条件 >)。在 SQL 术语中,谓词锁笼罩满足谓词的所有数据项以及 INSERT,UPDATE 或 DELETE 后满足谓词的所有数据项。不同事务的两个谓词锁中如果一个是写锁,并且两个锁笼罩了雷同的(可能是幻影)数据项,则两个谓词锁相抵触。数据项(item)锁(记录锁)是一个谓词锁,其中谓词指定特定记录。

事务具备好模式的写(读)(well-formed writes/reads)要求在写(读)该数据项或谓词定义的数据项集之前,每个数据项或谓词申请写锁(读锁)(译者注:也就是说在读(写)时对指定数据项集进行有且仅有一次的加读(写)锁)。事务是好模式(well-formed)的,要求事务有好模式的读与写。事务具备两阶段写(读)(two-phase writes/reads)要求在开释写(读)锁之后,在数据项上没有设置新的写(读)锁。事务是两阶段(two-phase)的,要求事务在开释一些锁之后不会申请任何新的锁(读或写锁)。

长锁(long duration)要求锁到事务提交或停止为止。否则,为短锁(short duration)。短锁通常在操作实现后立刻开释。

如果一个事务持有一个锁,另一个事务申请一个抵触的锁,那么在前一个事务的抵触锁曾经被开释之前,新的锁申请是不被授予的。

表 2 依据锁定范畴(数据项项或谓词),模式(读或写)及其持续时间(短或长)定义了多个隔离类型。基于锁的隔离级别:“锁读未提交”、“锁读已提交”、“锁可反复读”、“锁可串行化”是满足 ANSI SQL 隔离级别要求的,但表 2 与表 1 齐全不同,必须将基于锁定义的隔离级别与基于 ANSI SQL 景象的隔离级别进行辨别。为了辨别,表 2 中的级别标有“Locking”前缀,而不是表 1 的“ANSI”前缀。

ANSI SQL 景象的修改

上面重点剖析锁隔离级别与 ANSI SQL 的要求。这里先给出 P0 定义:

P0 (Dirty Write): Transaction T1 modifies a data item. Another transaction T2 then further modifies that data item before T1 performs a COMMIT or ROLLBACK. If T1 or T2 then performs a ROLLBACK, it is unclear what the correct data value should be.

形式化表白为:

P0: w1[x]…w2[x]…((c1 or a1) and (c2 or a2) in any order)

脏写不好的一个起因是它能够违反数据库一致性,并且在没有 P0 爱护的状况下,零碎无奈通过复原映像(image)来吊销更新(事务回滚)。因而,ANSI SQL 隔离应批改为要求所有隔离级别至多防止 P0 景象。

论文指出应该对 ANSI SQL 三个景象给出狭义的定义。先回顾 ANSI SQL 三个景象广义解释:

A1: w1[x]…r2[x]…(a1 and c2 in either order) (Dirty Read)

A2: r1[x]…w2[x]…c2…r1[x]…c1 (Fuzzy or Non-Repeatable Read)

A3: r1[P]…w2[y in P]…c2….r1[P]…c1 (Phantom)

给出三个广义解释不能囊括如下银行转账的场景:

H1: r1[x=50]w1[x=10]r2[x=10]r2[y=50]c2 r1[y=50]w1[y=90]c1

H2: r1[x=50]r2[x=50]w2[x=10]r2[y=50]w2[y=90]c2r1[y=90]c1

H3: r1[P] w2[insert y to P] r2[z] w2[z] c2 r1[z] c1

表 1 中展现,“读已提交”隔离的历史禁止景象 A1,“可反复读”隔离的历史禁止景象 A1 和 A2,“可串行化”隔离的历史禁止景象 A1,A2 和 A3。思考下面银行转账场景:

· 场景 1(H1):事务 T1 将 40 元从 x 转移到 y,要求放弃余额总数为 100,但 T2 读到了总余额为 60 的不统一状态。历史 H1 不违反任何异样 A1,A2 或 A3。然而狭义解释的 P1 解决这个问题。

· 场景 2(H2):事务 T2 看到总余额为 140,交易都没有读取脏(即未提交)的数据。因而 P1 满足。并且,没有任何数据项被读取两次,也没有谓词范畴内的数据被更改。H2 的问题是,当 T1 读取 y 时,x 的值已过期。如果 T2 再次读取 x,则会被更改。但因为 T2 不会读两次,A2 不实用。然而狭义解释的 P2 解决这个问题。

· 场景 3(H3):事务 T1 执行 < 搜寻条件 > 以找到雇员的列表。而后 T2 执行新的员工的插入,而后更新公司中的员工数量 z。尔后,T1 将员工的数量读出,并看到差别。这个历史显然是不可串行化的,但因为谓词范畴没有被拜访两次,所以它是被 A3 所容许的。然而狭义解释的 P3 解决这个问题。

综上,广义解释的 A1A2A3有意想不到的毛病,因而狭义解释的 P1P2P3更加正当。同时,ANSI SQL 隔离景象定义的不残缺,还有一些异样依然可能呈现,必须定义新的景象来实现锁的定义。此外,必须从新进行定义P3。狭义景象解释如下:

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)

留神,ANSI SQL 中 P3 只禁止对谓词插入(和更新),而下面的 P3 的定义禁止任何满足谓词的写被读取,这里的写能够是插入,更新或删除。

依据下面定义的景象,将 ANSI SQL 隔离级别从新定义,如表 3 所示:

对于单版本历史,容易得出 P0P1P2P3 景象是“假”的锁版本景象。理论,禁止 P0 排除了在第一个事务写入数据项后第二个事务的写,相当于在数据项(和谓词)上持有长写锁。所以脏写是不可能的。相似地,禁止 P1 相当于对数据项进行了好模式的读取。禁止 P2 示意数据项加上长读锁。最初,禁止 P3 意味着持有长谓词读锁。因而,表 3 中基于上述景象定义的隔离级别与表 2 的锁隔离级别是雷同的。换句话说,P0P1P2P3 是对于锁版本隔离级别的从新定义。

其余隔离类型

首先是游标稳固(cursor stability),游标稳固旨在避免失落更新景象。

P4 (Lost Update): The lost update anomaly occurs when transaction T1 reads a data item and then T2 updates the data item (possibly based on a previous read), then T1 (based on its earlier read value) updates the data item and commits. 将上述历史转化为:

P4: r1[x]…w2[x]…w1[x]…c1 (Lost Update)(留神 P4 只是基于 P0 脏写和 P1 脏读,从锁隔离的角度上来说只是持有短读锁和长写锁,没有达到锁可反复读 P2(也就是说不持有长读锁))

H4: r1[x=100] r2[x=100] w2[x=120] c2 w1[x=130] c1

如历史 H4 所示,问题是即便 T2 提交,T2 的更新也会失落。x 的最终值为 T1 写入的 130,P4 至多在读已提交隔离级别,因为禁止 P0(事务执行第一次写操作的数据项被另一个事务第二次写入)或 P1(写后提交前被读取)的状况下容许呈现 H4。当然,禁止 P2 也排除了 P4,因为 P2 是 r1[x],w2[x],(c1 or a1),包含了 P4。因而,P4 可用于作为辨别读已提交和可反复读强度两头的隔离级别。即 READ COMMITTED « Cursor Stability « REPEATABLE READ。

游标稳固扩大了读已提交隔离级别下对于 SQL 游标的锁行为。其提出游标上的 Fetching 操作 rc(read cursor)。rc 要求在游标的以后数据项上放弃长读锁,直到游标挪动或敞开(可能通过提交敞开)。当然,游标上的 Fetching 事务能够更新行(read cursor),即便游标在随后的 Fetch 上挪动,写锁也将放弃在行上直到事务提交。rc1[x] 和当前的 wc1[x] 排除了染指的 w2 [x]。因而,针对游标上的状况,提出景象 P4C

P4C:rc1[x] … w2[x] … w1[x] … c1(Lost Update)

其次是快照隔离(Snapshot Isolation)。

在快照隔离下执行的事务始终从事务开始时起的数据(已提交)的快照中读取数据。事务开始时获取的工夫戳称为其开始工夫戳(Start-Timestamp)。这一个工夫戳可能为事务第一次读之前的任何工夫。事务运行在快照隔离中时,只有能够保护其开始工夫戳对应的快照数据,在就不会阻塞读。事务的写入(更新,插入和删除)也将反映在此快照中,如果事务第二次拜访(即读取或更新)数据,则能再次读到。这个事务开始工夫戳之后的其余事务的更新对于本次事务是不可见的。

快照隔离是一种多版本并发管制(Multiversion Concurrency Control,MVCC)。当事务 T1 筹备好提交时,它将取得一个提交工夫戳(Commit-Timestamp),该值大于任何现有的工夫戳。当其余事务 T2 提交了数据的提交工夫戳在 T1 事务的距离 [Start-Timestamp,Commit-Timestamp] 中,只有 T1 与 T2 数据不重叠,事务才胜利提交。否则,T1 将停止。这个性能叫做先提交者胜利(First-Committer-Wins),避免失落更新(P4)。当 T1 提交时,其更改对于开始工夫戳大于 T1 的提交工夫戳的所有事务都可见。

快照隔离是一种多版本(MV)办法,因而单版本(SV)历史不能正确地反映工夫上的操作序列。在任何时候,每个数据项可能有多个版本,由流动的和已提交的事务写入。事务必须读取适合的版本。思考下面提到的历史 H1,其表明在单值执行中须要 P1。在快照隔离下,雷同的操作序列将导致多值历史:

H1.SI:

r1[x0=50] w1[x1=10] r2[x0=50] r2[y0=50] c2

· r1[y0=50] w1[y1=90] c1

将 MV 历史映射到 SV 历史是在隔离档次中搁置快照隔离的要害。例如,能够将 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 写入一个新值的每个事务有放弃 x + y>0 的束缚,而 T1 和 T2 两者都是隔离的,所以束缚不能放弃在 H5 中。

束缚违反(Constraint violation)是一种通用和重要的并发异样类型。个别数据库满足多个数据项的束缚(例如,键的唯一性,援用完整性,两个表中的行的复制关系等)。

它们一起造成数据库不变量束缚谓词 C(DB)。如果数据库状态 DB 与束缚统一,则不变量为 TRUE,否则为 FALSE。事务必须保留束缚谓词以放弃一致性:如果数据库在事务启动时保持一致,则事务提交时数据库将统一。如果事务读取到违反束缚谓词的数据库状态,则事务将受到束缚违反并发异样的影响。这种束缚违反在 [DAT] 中称为不统一剖析(inconsistent analysis)。

给出了几个相干的定义。

A5 (Data Item Constraint Violation). Suppose C() is a database constraint between two data items x and y in the database. 这里提出两个因为违反束缚引起的景象。

A5A Read Skew Suppose transaction T1 reads x, and then a second transaction T2 updates x and y to new values and commits. If now T1 reads y, it may see an inconsistent state, and therefore produce an inconsistent state as output. In terms of histories, we have the anomaly:

A5A: r1[x]…w2[x]…w2[y]…c2…r1[y]…(c1 or a1) (Read Skew)

A5B Write Skew Suppose T1 reads x and y, which are consistent with C(), and then a T2 reads x and y, writes x, and commits. Then T1 writes y. If there were a constraint between x and y, it might be violated. In terms of histories:

A5B: r1[x]…r2[y]…w1[y]…w2[x]…(c1 and c2 occur) (Write Skew)

不可反复度 P2 是读歪斜的进化模式,其中令 x =y。更典型地,事务读取两个不同但相干的我的项目(如援用完整性)。写歪斜(A5B)可能来自银行业务语义的束缚,如只有总共持有的余额放弃非负,账户余额能力变为负值。如历史 H5 中呈现的异样。

在排除 P2 的历史中,A5AA5B 都不会呈现,因为 A5AA5B 都有 T2 写入一个先前未被提交的 T1 读取的数据项的状况。因而,景象 A5AA5B 仅用于辨别低于可反复读取的隔离级别。

对于快照隔离,比读已提交更强,即 READ COMMITTED « Snapshot Isolation。

证实:在快照隔离中,first-committer-wins 排除了 P0(脏写入),并且工夫戳机制阻止了 P1(脏读),因而快照隔离不比读已提交弱。此外,A5A 可能在读已提交下,但不在快照隔离与工夫戳机制下。因而 READ COMMITTED « Snapshot Isolation。

在单版本景象中,难以描述快照隔离历史如何违反景象 P2。异样 A2 不能产生,因为快照隔离下的事务即便在另一个事务更新数据项之后也会只读取数据项的雷同版本对应的值。然而偏写(A5B)显然会产生在快照隔离下(比方H5),并且在单值历史解释中曾经提到,禁止了 P2 也会排除 A5B。因而,快照隔离抵赖可反复读没有历史异样。

快照隔离下不会产生 A3 异样(幻读)。在一个事务更新数据项集时,另一个事务屡次谓词读的将始终看到雷同的旧数据项集。然而可反复读隔离级别可能会遇到 A3 异样。快照隔离禁止具备异样 A3 的历史,但容许 A5B,而可反复读则相同(容许 A3 禁止 A5B)。因而,REPEATABLE READ »« Snapshot Isolation。

然而,快照隔离(能排除 A3)并不排除 P3(谓词读事务提交前谓词范畴内被另一事务写入)。思考一个束缚,示意由谓词确定的一组作业工作不能有大于 8 的小时数。T1 读取此谓词,确定总和只有 7 小时,并增加 1 小时持续时间的新工作,而并发事务 T2 做同样的事件。因为两个事务正在插入不同的数据项(以及不同的索引条目(如果有的话)),因而 First-Committer-Wins 不排除此状况,并且可能产生在快照隔离中。然而在任何等价的串行历史中,在这种状况下会呈现 P3 景象。

另外,快照隔离没有幻读(在 ANSI SQL 中广义定义下的 A3),因为每个事务都不会看到并发事务的更新。快照隔离历史排除了景象 A1A2A3。因而,在表 1 中的异样可串行化(ANOMALY SERIALIZABLE)的解释语境下:ANOMALY SERIALIZABLE « SNAPSHOT ISOLATION。

快照隔离的“乐观”并发管制办法对于只读事务具备显著的并发劣势,但其对更新事务的益处依然存在争议。

表 4 展现了上述提到的所有的隔离级别以及对应的景象:

综上,作者认为原始 ANSI SQL 隔离级别的定义存在重大问题。英文文字上的定义是含糊和不残缺的,脏写 P0 没有被排除。同时倡议 ANSI SQL 隔离级别替换为对应锁隔离级别。同时将各种商业数据库中实现的隔离级别进行比对,对应关系如图 2 所示:

以上内容次要参考论文 A Critique of ANSI SQL Isolation Levels。

(to be continued.)

退出移动版