EagleBear2002 的博客

这里必须根绝一切犹豫,这里任何怯懦都无济于事

Transactional Information Systems

摘要

SQL Isolation Levels

上一节讨论了手动控制每个应用程序甚至每个事务的锁定持续时间的想法,通过仅允许应用程序架构师或开发人员选择有限数量的“锁定样式”选项,可以使其更加安全。这些选项称为隔离级别,其中之一就是通常的冲突可串行化概念,它们甚至已被纳入 SQL 标准。通过发出相应的 SQL 命令“set isolation level ...”来选择特定的隔离级别

SQL 标准支持的隔离级别是根据与(强)严格两阶段锁定的受控偏差来定义的。然而,它们应该被理解为调度类,尽管是由特定的锁定协议生成的,但可以通过服务器选择使用的任何适当的并发控制算法来强制执行。实际上最重要的隔离级别定义如下:

定义 10.1 隔离级别

如果根据 S2PL 获取和释放写锁,即所有写锁都保持到事务结束,则称调度 \(s\) 在隔离级别读未提交(Read Uncomitted,也称为脏读或浏览级别)下运行。

如果根据 S2PL 获取和释放写锁,并且读锁(至少)在客户端发出的每个数据服务器操作的持续时间内保持,则称调度 \(s\) 在隔离级别读已提交(Read Comitted,也称为游标稳定性级别)下运行。

如果调度 \(s\) 可以通过 S2PL 协议生成,则称调度 s 在隔离级别(冲突)可串行化(SER)下运行。

这三个隔离级别在共享锁和独占锁的锁定持续时间方面有所不同。本质上,未提交读锁不需要任何读锁,并且对于不需要数据一致性视图的单纯浏览或统计评估很有用(例如,计算几千本书的平均价格,其中在统计分析期间价格发生变化的几本书并不重要)。长持续时间的写锁表面上似乎可以防止持久数据变得不一致,但实际上这无法保证,因为写入的数据值现在可能取决于任意不一致的甚至是“脏的”(即未提交并随后中止的)读取。

已提交读锁在一定程度上缓解了此类问题:特别是,它消除了脏读异常。它的特点是读锁短,写锁长。因此,它减少了可能的数据争用,尤其是在长读取和短更新事务之间。这是实践中特别有用且广泛使用的选项,但必须极其谨慎地使用,因为它实际上可能导致持久数据不一致。事实上,读取已提交隔离级别仍然容易受到丢失更新异常的影响,如以下示例所示:

\[ r_1(x)r_2(x)w_2(x)c_2w_1(x)c_1 \]

一些商业数据服务器甚至 SQL 标准进一步区分了完全(冲突)可串行化级别和宽松、临时形式的可串行化,后者可以容忍幻影问题,但“不能容忍其他”类型的不一致。后者通常被称为“可重复读取”级别,尽管有时也用作可串行化的同义词。这个临时概念没有精确定义,因为“其他”不一致的类别有些模糊。

如前所述,上述隔离级别是根据锁定规则定义的,但我们当然可以设计其他类型的并发控制算法来强制执行特定的隔离级别。如果我们想要使用多版本并发控制协议,锁定规则所依据的冲突概念不会直接延续。为此,一些商业系统引入了额外的隔离级别,这些隔离级别假设了多版本并发控制算法并指定了对多版本可串行化的受控放松。

定义 10.2 多版本读取已提交和快照隔离级别

在多版本读取已提交隔离级别下运行的事务 多版本读取已提交,快照隔离读取在发出读取操作时提交的请求数据项的最新版本。事务可能调用的所有写入都受(非版本)排他锁定的约束,锁定将一直保持到事务终止。

对于在快照隔离级别下运行的事务,所有操作都会读取自事务开始时以来的最新版本。此外,每对并发事务的写入集必须是不相交的。

在这两个级别中,快照隔离是较强的。虽然多版本读取已提交仍然容易丢失更新(尽管它具有一致性读取),但快照隔离非常接近完全可序列化。事实上,如果它仅应用于只读事务,则所有事务都会具有一致的数据视图,并且可以保证持久数据本身保持一致。通过观察第 5 章中介绍的只读多版本 (ROMV) 协议生成的历史记录与上述快照隔离定义完全匹配,可以很容易地看出这一点。但是,以下历史记录(数据项下标表示版本)显示更新事务的快照隔离可能导致不可多版本序列化的情况:

\[ r_1(x_0)r_1(y_0)r_2(x_0)r_2(y_0)w_1(x_1)c_1w_2(y_2)c_2 \] 作为具体解释,假设 x 和 y 是两个数值数据项,它们的初始值均为 5,并且由一致性约束 x + y ≥ 0 相互关联。现在想象两个并发事务,它们都读取 x 和 y 并从两个数据项之一中减去 10。在读取未提交级别下,这可能导致上述历史记录,该历史记录不可序列化,并且会导致两个数据项的值均为 -5,从而违反一致性约束。

另一方面,快照隔离的一个优点是它可以被形式化并以严格的方式进行研究。我们的上述定义可以转化为多版本并发控制理论(参见第 5 章),如下所示:

定义 10.3 快照隔离的形式化定义

快照隔离调度类和 MVSR 类是无法比较的。一方面,快照隔离是 MVSR 的一个特例,因为它使用特定的版本函数,并且由于写集不相交条件而更加严格。另一方面,快照隔离不需要与串行单版本调度兼容的读取关系;在这方面它比 MVSR 更自由。

快照隔离的标准可以通过图形构造轻松表征:

定义 10.4 快照隔离序列化图

多版本历史 s 的快照隔离序列化图是一个有向图,以事务为节点,并有以下边: 对于计划中的每个操作 rj(xi),都有一条边 ti → tj,标记为 x。 对于每对操作 rk(xj) 和 wi(xi),都有一条边 1. 如果 ci < cj,则 ti → tj 和 2. 如果 cj < ci,则 tk → ti,在两种情况下都标记为 x

定理 10.2

假设 s 是一个多版本计划,它具有根据快照隔离标准的版本函数,并且在同一事务中,对对象 x s 的每个写入步骤都先于对 x 的读取步骤。则以下情况成立:

  1. 当且仅当其对应的快照隔离序列化图是非循环的,则 s 是 MVSR;
  2. 当且仅当不存在对象 x,使得对应的快照隔离图具有仅由标记为 x 的边组成的循环时,s 是快照隔离的。

可以使用第 5 章中介绍的 ROMV 协议实现快照隔离,适用于所有读取操作(即,甚至是更新事务的读取操作),并附加一个机制来确保并发事务的写入集不相交。后者的一个直接方法是记住写入集并在事务的提交请求时执行显式不相交测试。如果测试表明重叠,则中止事务。一种更好的方法是像往常一样获取写入锁,并根据先前提交的并发事务的写入集检查这些锁,它可以更早地检测到不相交,从而避免浪费最终会被中止的事务中的工作。请注意,这需要的不仅仅是通常的锁冲突测试,因为已经终止的事务不再持有其锁;相反,我们需要考虑它们在提交时持有的写锁。