SQL标准定义了四种隔离级别。最严格的是可序列化,在标准中用了一整段来定义它,其中说到一组可序列化事务的任意并发执行被保证效果和以某种顺序一个一个执行这些事务一样。其他三种级别使用并发事务之间交互产生的现象来定义,每一个级别中都要求必须不出现一种现象。注意由于可序列化的定义,在该级别上这些现象都不可能发生(这并不令人惊讶--如果事务的效果与每个时刻只运行一个的相同,你怎么可能看见由于交互产生的现象?)。
在各个级别上被禁止出现的现象是:
SQL 标准和 LightDB 实现的事务隔离级别在 Table 14.1中描述。
Table 14.1. 事务隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 | 序列化异常 |
---|---|---|---|---|
读未提交 | 允许,但不在 LightDB 中 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 允许,但不在 LightDB 中 | 可能 |
可序列化 | 不可能 | 不可能 | 不可能 | 不可能 |
在LightDB中,你可以请求四种标准事务隔离级别中的任意一种,但是内部只实现了三种不同的隔离级别,即 LightDB 的读未提交模式的行为和读已提交相同。这是因为把标准隔离级别映射到 LightDB 的多版本并发控制架构的唯一合理的方法。
该表格也显示 LightDB 的可重复读实现不允许幻读。而 SQL 标准允许更严格的行为:四种隔离级别只定义了哪种现像不能发生,但是没有定义哪种现像必须发生。可用的隔离级别的行为在下面的小节中详细描述。
要设置一个事务的事务隔离级别,使用SET TRANSACTION命令。
某些LightDB数据类型和函数关于事务的行为有特殊的规则。特别是,对一个序列的修改(以及用serial
声明的一列的计数器)是立刻对所有其他事务可见的,并且在作出该修改的事务中断时也不会被回滚。见Section 10.16和Section 9.1.4。
读已提交是LightDB中的默认隔离级别。 当一个事务运行使用这个隔离级别时, 一个查询(没有FOR UPDATE/SHARE
子句)只能看到查询开始之前已经被提交的数据, 而无法看到未提交的数据或在查询执行期间其它事务提交的数据。实际上,SELECT
查询看到的是一个在查询开始运行的瞬间该数据库的一个快照。不过SELECT
可以看见在它自身事务中之前执行的更新的效果,即使它们还没有被提交。还要注意的是,即使在同一个事务里两个相邻的SELECT
命令可能看到不同的数据, 因为其它事务可能会在第一个SELECT
开始和第二个SELECT
开始之间提交。
UPDATE
、DELETE
、SELECT FOR UPDATE
和SELECT FOR SHARE
命令在搜索目标行时的行为和SELECT
一样: 它们将只找到在命令开始时已经被提交的行。 不过,在被找到时,这样的目标行可能已经被其它并发事务更新(或删除或锁住)。在这种情况下, 即将进行的更新将等待第一个更新事务提交或者回滚(如果它还在进行中)。 如果第一个更新事务回滚,那么它的作用将被忽略并且第二个事务可以继续更新最初发现的行。 如果第一个更新事务提交,若该行被第一个更新者删除,则第二个更新事务将忽略该行,否则第二个更新者将试图在该行的已被更新的版本上应用它的操作。该命令的搜索条件(WHERE
子句)将被重新计算来看该行被更新的版本是否仍然符合搜索条件。如果符合,则第二个更新者使用该行的已更新版本继续其操作。在SELECT FOR UPDATE
和SELECT FOR SHARE
的情况下,这意味着把该行的已更新版本锁住并返回给客户端。
带有ON CONFLICT DO UPDATE
子句的
INSERT
行为类似。在读已提交模式,要插入的
每一行将被插入或者更新。除非有不相干的错误出现,这两种结果之一是肯定
会出现的。如果在另一个事务中发生冲突,并且其效果对于INSERT
还不可见,则UPDATE
子句将会
影响那个行,即便那一行对于该命令来说没有惯常的可见版本。
带有ON CONFLICT DO NOTHING
子句的
INSERT
有可能因为另一个效果对
INSERT
快照不可见的事务的结果无法让插入进行
下去。再一次,这只是读已提交模式中的情况。
因为上面的规则,正在更新的命令可能会看到一个不一致的快照: 它们可以看到并发更新命令在它尝试更新的相同行上的作用,但是却看不到那些命令对数据库里其它行的作用。 这样的行为令读已提交模式不适合用于涉及复杂搜索条件的命令。不过,它对于更简单的情况是正确的。 例如,考虑用这样的命令更新银行余额:
BEGIN; UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345; UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534; COMMIT;
如果两个这样的事务同时尝试修改帐号 12345 的余额,那我们很明显希望第二个事务从账户行的已更新版本上开始工作。 因为每个命令只影响一个已经决定了的行,让它看到行的已更新版本不会导致任何麻烦的不一致性。
在读已提交模式中,更复杂的使用可能产生不符合需要的结果。例如: 考虑一个在数据上操作的DELETE
命令,它操作的数据正被另一个命令从它的限制条件中移除或者加入,例如,假定website
是一个两行的表,两行的website.hits
等于9
和10
:
BEGIN; UPDATE website SET hits = hits + 1; -- run from another session: DELETE FROM website WHERE hits = 10; COMMIT;
即便在UPDATE
之前有一个website.hits = 10
的行,DELETE
将不会产生效果。这是因为更新之前的行值9
被跳过,并且当UPDATE
完成并且DELETE
获得一个锁,新行值不再是10
而是11
,这再也不匹配条件了。
因为在读已提交模式中,每个命令都是从一个新的快照开始的,而这个快照包含在该时刻已提交的事务, 因此同一事务中的后续命令将看到任何已提交的并行事务的效果。以上的焦点在于单个命令是否看到数据库的绝对一致的视图。
读已提交模式提供的部分事务隔离对于许多应用而言是足够的,并且这个模式速度快并且使用简单。 不过,它不是对于所有情况都够用。做复杂查询和更新的应用可能需要比读已提交模式提供的更严格一致的数据库视图。
可重复读隔离级别只看到在事务开始之前被提交的数据;它从来看不到未提交的数据或者并行事务在本事务执行期间提交的修改(不过,查询能够看见在它的事务中之前执行的更新,即使它们还没有被提交)。这是比SQL标准对此隔离级别所要求的更强的保证,并且阻止Table 14.1中描述的除了序列化异常之外的所有现象。如上面所提到的,这是标准特别允许的,标准只描述了每种隔离级别必须提供的最小保护。
这个级别与读已提交不同之处在于,一个可重复读事务中的查询可以看见在事务中第一个非事务控制语句开始时的一个快照,而不是事务中当前语句开始时的快照。因此,在一个单一事务中的后续SELECT
命令看到的是相同的数据,即它们看不到其他事务在本事务启动后提交的修改。
使用这个级别的应用必须准备好由于序列化失败而重试事务。
UPDATE
、DELETE
、SELECT FOR UPDATE
和SELECT FOR SHARE
命令在搜索目标行时的行为和SELECT
一样: 它们将只找到在事务开始时已经被提交的行。 不过,在被找到时,这样的目标行可能已经被其它并发事务更新(或删除或锁住)。在这种情况下, 可重复读事务将等待第一个更新事务提交或者回滚(如果它还在进行中)。 如果第一个更新事务回滚,那么它的作用将被忽略并且可重复读事务可以继续更新最初发现的行。 但是如果第一个更新事务提交(并且实际更新或删除该行,而不是只锁住它),则可重复读事务将回滚并带有如下消息
ERROR: could not serialize access due to concurrent update
因为一个可重复读事务无法修改或者锁住被其他在可重复读事务开始之后的事务改变的行。
当一个应用接收到这个错误消息,它应该中断当前事务并且从开头重试整个事务。在第二次执行中,该事务将见到作为其初始数据库视图一部分的之前提交的改变,这样在使用行的新版本作为新事务更新的起点时就不会有逻辑冲突。
注意只有更新事务可能需要被重试;只读事务将永远不会有序列化冲突。
可重复读模式提供了一种严格的保证,在其中每一个事务看到数据库的一个完全稳定的视图。不过,这个视图并不需要总是和同一级别上并发事务的某些序列化(一次一个)执行保持一致。例如,即使这个级别上的一个只读事务可能看到一个控制记录被更新,这显示一个批处理已经被完成但是不能看见作为该批处理的逻辑组成部分的一个细节记录,因为它读取空值记录的一个较早的版本。如果不小心地使用显式锁来阻塞冲突事务,尝试用运行在这个隔离级别的事务来强制业务规则不太可能正确地工作。
可重复读隔离级别是使用学术数据库文献和一些其他数据库产品中称为Snapshot Isolation的已知的技术来实现的。 与使用传统锁技术并降低并发性的系统相比,可以观察到行为和性能方面的差异。 一些其他系统甚至可以提供可重复读取和快照隔离作为具有不同行为的不同隔离级别。
目前,一个对于可序列化事务隔离级别的请求会提供和这里描述的完全一样的行为。