11. 并发控制

11.1. 介绍

瀚高数据库为开发者提供了一组丰富的工具来管理对数据的并发访问。在内部,数据一致性通过使用一种多版本模型(多版本并发控制,MVCC)来维护。这就意味着每个 SQL 语句看到的都只是一小段时间之前的数据快照(一个数据库版本),而不管底层数据的当前状态。这样可以保护语句不会看到可能由其他在相同数据行上执行更新的并发事务造成的不一致数据,为每一个数据库会话提供事务隔离。MVCC避免了传统的数据库系统的锁定方法,将锁争夺最小化来允许多用户环境中的合理性能。

使用MVCC并发控制模型而不是锁定的主要优点是在MVCC中,对查询(读)数据的锁请求与写数据的锁请求不冲突,所以读不会阻塞写,而写也从不阻塞读。甚至在通过使用革新的可序列化快照隔离(SSI)级别提供最严格的事务隔离级别时,瀚高数据库也维持这个保证。

在瀚高数据库里也有表和行级别的锁功能,用于那些通常不需要完整事务隔离并且想要显式管理特定冲突点的应用。不过,恰当地使用MVCC通常会提供比锁更好的性能。另外,由应用定义的咨询锁提供了一个获得不依赖于单一事务的锁的机制。

11.2. 事务隔离

SQL标准定义了四种隔离级别。最严格的是可序列化,在标准中用了一整段来定义它,其中说到一组可序列化事务的任意并发执行被保证效果和以某种顺序一个一个执行这些事务一样。其他三种级别使用并发事务之间交互产生的现象来定义,每一个级别中都要求必须不出现一种现象。注意由于可序列化的定义,在该级别上这些现象都不可能发生。

在各个级别上被禁止出现的现象是:

脏读

一个事务读取了另一个并行未提交事务写入的数据。

不可重复读

一个事务重新读取之前读取过的数据,发现该数据已经被另一个事务(在初始读之后提交)修改。

幻读

一个事务重新执行一个返回符合一个搜索条件的行集合的查询,发现满足条件的行集合因为另一个最近提交的事务而发生了改变。

序列化异常

成功提交一组事务的结果与这些事务所有可能的串行执行结果都不一致。

SQL 标准和瀚高数据库实现的事务隔离级别在下表中描述。

表 12.1 事务隔离级别

隔离级别 脏读 不可重复读 幻读 序列化异常
读未提交 允许,但不在 瀚高数据库中 可能 可能 可能
读已提交 不可能 可能 可能 可能
可重复读 不可能 不可能 允许,但不在瀚高数据库中 可能
可序列化 不可能 不可能 不可能 不可能

在瀚高数据库中,你可以请求四种标准事务隔离级别中的任意一种,但是内部只实现了三种不同的隔离级别,即瀚高数据库的读未提交模式的行为和读已提交相同。这是因为把标准隔离级别映射到瀚高数据库的多版本并发控制架构的唯一合理的方法。

该表格也显示瀚高数据库的可重复读实现不允许幻读。而 SQL 标准允许更严格的行为:四种隔离级别只定义了哪种现像不能发生,但是没有定义哪种现像必须发生。可用的隔离级别的行为在下面的小节中详细描述。

要设置一个事务的事务隔离级别,使用SET TRANSACTION命令。

重要:
某些瀚高数据库数据类型和函数关于事务的行为有特殊的规则。特别是,对一个序列的修改(以及用serial声明的一列的计数器)是立刻对所有其他事务可见的,并且在作出该修改的事务中断时也不会被回滚。

11.2.1. 读已提交隔离级别

读已提交是瀚高数据库中的默认隔离级别。当一个事务运行使用这个隔离级别时,一个查询(没有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,这再也不匹配条件了。

因为在读已提交模式中,每个命令都是从一个新的快照开始的,而这个快照包含在该时刻已提交的事务,因此同一事务中的后续命令将看到任何已提交的并行事务的效果。以上的焦点在于单个命令是否看到数据库的绝对一致的视图。

读已提交模式提供的部分事务隔离对于许多应用而言是足够的,并且这个模式速度快并且使用简单。不过,它不是对于所有情况都够用。做复杂查询和更新的应用可能需要比读已提交模式提供的更严格一致的数据库视图。

11.2.2. 可重复读隔离级别

可重复读隔离级别只看到在事务开始之前被提交的数据;它从来看不到未提交的数据或者并行事务在本事务执行期间提交的修改(不过,查询能够看见在它的事务中之前执行的更新,即使它们还没有被提交)。这是比SQL标准对此隔离级别所要求的更强的保证,并且阻止除了序列化异常之外的所有现象。如上面所提到的,这是标准特别允许的,标准只描述了每种隔离级别必须提供的最小保护。

这个级别与读已提交不同之处在于,一个可重复读事务中的查询可以看见在事务中第一个非事务控制语句开始时的一个快照,而不是事务中当前语句开始时的快照。因此,在一个单一事务中的后续SELECT命令看到的是相同的数据,即它们看不到其他事务在本事务启动后提交的修改。

使用这个级别的应用必须准备好由于序列化失败而重试事务。

UPDATE、DELETE、SELECT FOR UPDATE和SELECT FOR SHARE命

令在搜索目标行时的行为和SELECT一样: 它们将只找到在事务开始时已经被提交的行。不过,在被找到时,这样的目标行可能已经被其它并发事务更新(或删除或锁住)。

在这种情况下,可重复读事务将等待第一个更新事务提交或者回滚(如果它还在进行中)。如果第一个更新事务回滚,那么它的作用将被忽略并且可重复读事务可以继续更新最初发现的行。但是如果第一个更新事务提交(并且实4际更新或删除该行,而不是只锁住它),则可重复读事务将回滚并带有如下消息 ERROR: could not serialize access due to concurrent update

因为一个可重复读事务无法修改或者锁住被其他在可重复读事务开始之后的事务改变的行。

当一个应用接收到这个错误消息,它应该中断当前事务并且从开头重试整个事务。在第二次执行中,该事务将见到作为其初始数据库视图一部分的之前提交的改变,这样在使用行的新版本作为新事务更新的起点时就不会有逻辑冲突。

注意只有更新事务可能需要被重试;只读事务将永远不会有序列化冲突。

可重复读模式提供了一种严格的保证,在其中每一个事务看到数据库的一个完全稳定的视图。不过,这个视图并不需要总是和同一级别上并发事务的某些序列化(一次一个)执行保持一致。例如,即使这个级别上的一个只读事务可能看到一个控制记录被更新,这显示一个批处理已经被完成但是不能看见作为该批处理的逻辑组成部分的一个细节记录,因为它读取空值记录的一个较早的版本。

11.2.3. 可序列化隔离级别

可序列化隔离级别提供了最严格的事务隔离。这个级别为所有已提交事务模拟序列事务执行;就好像事务被按照序列一个接着另一个被执行,而不是并行地被执行。但是,和可重复读级别相似,使用这个级别的应用必须准备好因为序列化失败而重试事务。

例如,考虑一个表mytab,它初始时包含:

class | value

——-+——-

1 | 10

1 | 20

2 | 100

2 | 200

假设可序列化事务 A 计算:

SELECT SUM(value) FROM mytab WHERE class = 1;

并且接着把结果(3)作为一个新行的value插入,新行的class = 2。同时,可序列化事务 B

计算:

SELECT SUM(value) FROM mytab WHERE class = 2;

并得到结果 300,它会将其与class = 1插入到一个新行中。然后两个事务都尝试提交。如果其中一个事务运行在可重复读隔离级别,两者都被允许提交;但是由于没有执行的序列化顺序能在结果上一致,使用可序列化事务将允许一个事务提交并且将回滚另一个并伴有这个消息:

ERROR: could not serialize access due to read/write dependencies among

Transactions

这是因为,如果 A 在 B 之前执行,B 将计算得到合计值 330 而不是 300,而且相似地另一种顺序将导致 A 计算出一个不同的合计值。

当依赖可序列化事务来阻止异常时,重要的一点是任何从一个持久化用户表读出数据都不被认为是有效的,直到读它的事务已经成功提交为止。即便是对只读事务也是如此,除了在一个可推迟的只读事务中读取的数据是读出以后立刻有效的,因为这样的一个事务在开始读取任何数据之前会等待,直到它能获得一个快照保证来避免这种问题为止。在所有其他情况下,应用不能依靠在一个后来被中断的事务中读取的结果;相反,它们应当重试事务直到它成功。

要保证真正的可序列化,瀚高数据库使用了谓词锁,这意味着它会保持锁,这些锁让它能够判断在它先运行的情况下,什么时候一个写操作会对一个并发事务中之前读取的结果产生影响。在瀚高数据库中,这些锁并不导致任何阻塞,并且因此不会导致一个死锁。它们被用来标识和标志并发可序列化事务之间的依赖性,这些事务的组合可能导致序列化异常。相反,一个想要保证数据一致性的读已提交或可重复读事务可能需要拿走一个在整个表上的锁,这可能阻塞其他尝试使用该表的用户,或者它可能会使用不仅会阻塞其他事务还会导致磁盘访问的SELECT FOR UPDATE或SELECT FOR SHARE。

像大部分其他数据库系统,瀚高数据库中的谓词锁基于被一个事务真正访问的数据。这些谓词锁将显示在pg_locks系统视图中,它们的mode为SIReadLock。这种在一个查询执行期间获得的特别的锁将依赖于该查询所使用的计划,并且在事务过程中多个细粒度锁(如元组锁)可能和少量粗粒度锁(如页面锁)相结合来防止耗尽用于跟踪锁的内存。如果一个READ ONLY事务检测到不会有导致序列化异常的冲突发生,它可以在完成前释放其 SIRead 锁。事实上,READ ONLY事务将常常可以在启动时确立这一事实并避免拿到任何谓词锁。如果你显式地请求一个SERIALIZABLE READ ONLY DEFERRABLE事务,它将阻塞直到它能够确立这一事实(这是唯一一种可序列化事务阻塞但可重复读事务不阻塞的情况)。在另一方面,SIRead锁常常需要被保持到事务提交之后,直到重叠的读写事务完成。

坚持使用可序列化事务可以简化开发。成功提交的并发可序列化事务的任意集合将得到和一次运行一个相同效果的这种保证意味着,如果你能证明一个单一事务在独自运行时能做正确的事情,则你可以相信它在任何混合的可序列化事务中也能做正确的事情,即使它不知道那些其他事务做了些什么,否则它将不会成功提交。重要的是使用这种技术的环境有一种普遍的方法来处理序列化失败(总是会返回一个 SQLSTATE 值 ‘40001’),因为它将很难准确地预计哪些事务可能为读/写依赖性做贡献并且需要被回滚来阻止序列化异常。读/写依赖性的监控会产生开销,如重启被序列化失败中止的事务,但是作为在该开销和显式锁及SELECT FOR UPDATE或SELECT FOR SHARE导致的阻塞之间的一种平衡,可序列化事务是在某些环境中最好性能的选择。

虽然瀚高数据库的可序列化事务隔离级别只允许并发事务在能够证明有一种串行执行能够产生相同效果的前提下提交,但它在真正的串行执行中仍然会产生一些错误。

尤其是可能会看到由于可序列化事务重叠执行导致的唯一约束被违背的情况,这些情况即便在尝试插入键之前就显式地检查过该键不存在也会发生。避免这种问题的方法是,确保所有插入可能会冲突的键的可序列化事务首先显式地检查它们能不能那样做。例如,试想一个要求用户输入新键的应用,它会通过尝试查询用户给出的键来检查键是否已经存在,或者是通过选取现有最大的键并且加一来产生一个新键。如果某些可序列化事务不遵循这种协议而直接插入新键,则也可能会报告唯一约束被违背,即便在并发事务串行执行的情况下不会发生唯一约束被违背也是如此。

当依赖可序列化事务进行并发控制时,为了最佳性能应该考虑一下问题:

• 在可能时声明事务为READ ONLY。

• 控制活动连接的数量,如果需要使用一个连接池。这总是一个重要的性能考虑,但是在一个使用可序列化事务的繁忙系统中这尤为重要。

• 只在一个单一事务中放完整性目的所需要的东西。

• 不要让连接不必要地“闲置在事务中”。配置参数idle_in_transaction_session_timeout可以被用来自动断开拖延会话的连接.

• 在那些由于使用可序列化事务自动提供的保护的地方消除不再需要的显式锁、SELECT FOR UPDATE和SELECT FOR SHARE。

• 当系统因为谓词锁表内存短缺而被强制结合多个页面级谓词锁为一个单一的关系级谓词锁时,序列化失败的比例可能会上升。你可以通过增加max_pred_locks_per_transaction、max_pred_locks_per_relation和max_pred_locks_per_page来避免这种情况。

• 一次顺序扫描将总是需要一个关系级谓词锁。这可能导致序列化失败的比例上升。通过缩减random_page_cost和/或增加cpu_tuple_cost来鼓励使用索引扫描将有助于此。一定要在事务回滚和重启数目的任何减少与查询执行时间的任何全面改变之间进行权衡。

11.3. 显式锁定

瀚高数据库提供了多种锁模式用于控制对表中数据的并发访问。这些模式可以用于在MVCC无法给出期望行为的情境中由应用控制的锁。同样,大多数瀚高数据库命令会自动要求恰当的锁以保证被引用的表在命令的执行过程中不会以一种不兼容的方式删除或修改(例如,TRUNCATE无法安全地与同一表中上的其他操作并发地执行,因此它在表上获得一个排他锁来强制这种行为)。

要检查在一个数据库服务器中当前未解除的锁列表,可以使用pg_locks系统视图。

11.3.1. 表级锁

下面的列表显示了可用的锁模式和瀚高数据库自动使用它们的场合。你也可以用LOCK命令显式获得这些锁。请记住所有这些锁模式都是表级锁,即使它们的名字包含“row”单词。在一定程度上,这些名字反应了每种锁模式的典型用法 — 但是语意却都是一样的。两种锁模式之间真正的区别是它们有着不同的冲突锁模式集合。两个事务在同一时刻不能在同一个表上持有属于相互冲突模式的锁。非冲突锁模式可以由许多事务同时持有。请特别注意有些锁模式是自冲突的(例如,在一个时刻ACCESS EXCLUSIVE锁不能被多于一个事务持有)而其他锁模式不是自冲突的(例如,ACCESS SHARE锁可以被多个事务持有)。

表级锁模式

ACCESS SHARE

只与ACCESS EXCLUSIVE锁模式冲突。

SELECT命令在被引用的表上获得一个这种模式的锁。通常,任何只读取表而不修改它的查询都将获得这种锁模式。

ROW SHARE

与EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。

SELECT FOR UPDATE和SELECT FOR SHARE命令在目标表上取得一个这种模式的锁 (加上在被引用但没有选择FOR UPDATE/FOR SHARE的任何其他表上的ACCESS SHARE锁)。

ROW EXCLUSIVE

与SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。

命令UPDATE、DELETE和INSERT在目标表上取得这种锁模式(加上在任何其他被引用表上的ACCESS SHARE锁)。通常,这种锁模式将被任何修改表中数据的命令取得。

SHARE UPDATE EXCLUSIVE

与SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式保护一个表不受并发模式改变和VACUUM运行的影响。

由VACUUM(不带FULL)、ANALYZE、CREATE INDEX CONCURRENTLY、REINDEX 、CONCURRENTLY、 CREATE STATISTICS以及某些ALTER INDEX和 ALTER TABLE的其他形式获得(完整的详细请参考ALTER INDEX 和ALTER TABLE)。

SHARE

与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式保护一个表不受并发数据改变的影响。

由CREATE INDEX(不带CONCURRENTLY)取得。

SHARE ROW EXCLUSIVE

与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式保护一个表不受并发数据修改所影响,并且是自排他的,这样在一个时刻只能有一个会话持有它。

由CREATE TRIGGER和某些形式的 ALTER TABLE所获得(见 ALTER TABLE)。

EXCLUSIVE

与ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW

EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式只允许并发的ACCESS SHARE锁,即只有来自于表的读操作可以与一个持有该锁模式的事务并行处理。

由REFRESH MATERIALIZED VIEW CONCURRENTLY获得。

ACCESS EXCLUSIVE

与所有模式的锁冲突(ACCESS SHARE、ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE)。这种模式保证持有者是访问该表的唯一事务。

由ALTER TABLE、DROP TABLE、TRUNCATE、REINDEX、CLUSTER、VACUUM FULL和REFRESH MATERIALIZED VIEW(不带CONCURRENTLY)命令获取。很多形式的ALTER INDEX和ALTER TABLE也在这个层面上获得锁(见ALTER TABLE)。这也是未显式指定模式的LOCK TABLE命令的默认锁模式。

提示:
只有一个ACCESS EXCLUSIVE锁阻塞一个SELECT(不带FOR UPDATE/SHARE)语句。

一旦被获取,一个锁通常将被持有直到事务结束。但是如果在建立保存点之后才获得锁,那么在回滚到这个保存点的时候将立即释放该锁。这与ROLLBACK取消保存点之后所有的影响的原则保持一致。同样的原则也适用于在PL/pgSQL异常块中获得的锁:一个跳出块的错误将释放在块中获得的锁。

表 12.2 冲突的锁模式

当前的锁模式→
请求的锁↓
ACCESS SHARE ROW SHARE ROW EXCLUSIVE SHARE UPDATE EXCLUSIVE SHARE SHARE ROW EXCLUSIVE EXCLUSIVE ACCESS EXCLUSIVE
ACCESS SHARE X
ROW SHARE X X
ROW EXCLUSIVE X X X X
SHARE UPDATE EXCLUSIVE X X X X X
SHARE X X X X X
SHARE ROW EXCLUSIVE X X X X X X
EXCLUSIVE X X X X X X X
ACCESS EXCLUSIVE X X X X X X X X

11.3.2. 行级锁

除了表级锁以外,还有行级锁,在下文列出了行级锁以及在哪些情境下瀚高数据库会自动使用它们。行级锁的完整冲突表请见下表。注意一个事务可能会在相同的行上保持冲突的锁,甚至是在不同的子事务中。但是除此之外,两个事务永远不可能在相同的行上持有冲突的锁。行级锁不影响数据查询,它们只阻塞对同一行的写入者和加锁者。

行级锁模式

FOR UPDATE

FOR UPDATE会导致由SELECT语句检索到的行被锁定,就好像它们要被更新。这可以阻止它们被其他事务锁定、修改或者删除,一直到当前事务结束。也就是说其他尝试UPDATE、DELETE、SELECT FOR UPDATE、SELECT FOR NO KEY UPDATE、SELECT FOR SHARE或者SELECT FOR KEY SHARE这些行的事务将被阻塞,直到当前事务结束。反过来,SELECT FOR UPDATE将等待已经在相同行上运行以上这些命令的并发事务,并且接着锁定并且返回被更新的行(或者没有行,因为行可能已被删除)。不过,在一个REPEATABLE READ或SERIALIZABLE事务中,如果一个要被锁定的行在事务开始后被更改,将会抛出一个错误。进一步的讨论请见第 13.4 节。

任何在一行上的DELETE命令也会获得FOR UPDATE锁模式,在某些列上修改值的UPDATE也会获得该锁模式。当前UPDATE情况中被考虑的列集合是那些具有能用于外键的唯一索引的列(所以部分索引和表达式索引不被考虑),但是这种要求未来有可能会改变。

FOR NO KEY UPDATE

行为与FOR UPDATE类似,不过获得的锁较弱:这种锁将不会阻塞尝试在相同行上获得锁的SELECT FOR KEY SHARE命令。任何不获取FOR UPDATE锁的UPDATE也会获得这种锁模式。

FOR SHARE

行为与FOR NO KEY UPDATE类似,不过它在每个检索到的行上获得一个共享锁而不是排他锁。一个共享锁会阻塞其他事务在这些行上执行UPDATE、DELETE、SELECT FOR UPDATE或者SELECT FOR NO KEY UPDATE,但是它不会阻止它们执行SELECT FOR SHARE或者SELECT FOR KEY SHARE。

FOR KEY SHARE

行为与FOR SHARE类似,不过锁较弱:SELECT FOR UPDATE会被阻塞,但是SELECT FOR NO KEY UPDATE不会被阻塞。一个键共享锁会阻塞其他事务执行修改键值的DELETE或者UPDATE,但不会阻塞其他UPDATE,也不会阻止SELECT FOR NO KEY UPDATE、SELECT FOR

SHARE或者SELECT FOR KEY SHARE。

瀚高数据库不会在内存里保存任何关于已修改行的信息,因此对一次锁定的行数没有限制。

不过,锁住一行会导致一次磁盘写,例如,SELECT FOR UPDATE将修改选中的行以标记它们被锁住,并且因此会导致磁盘写入。

表 12.3 冲突的行级锁

当前的锁模式→
请求的锁模式↓
FOR KEY SHARE FOR SHARE FOR NO KEY UPDATE FOR UPDATE
FOR KEY SHARE X
FOR SHARE X X
FOR NO KEY UPDATE X X X
FOR UPDATE X X X X

11.3.3. 页级锁

除了表级别和行级别的锁以外,页面级别的共享/排他锁被用来控制对共享缓冲池中表页面的读/写。这些锁在行被抓取或者更新后马上被释放。应用开发者通常不需要关心页级锁,我们在这里提到它们存在此概念。

11.3.4. 死锁

显式锁定的使用可能会增加死锁的可能性,死锁是指两个(或多个)事务相互持有对方想要的锁。例如,如果事务 1 在表 A 上获得一个排他锁,同时试图获取一个在表 B 上的排他锁,而事务 2 已经持有表 B 的排他锁,同时却正在请求表 A 上的一个排他锁,那么两个事务就都不能进行下去。瀚高数据库能够自动检测到死锁情况并且会通过中断其中一个事务从而允许其它事务完成来解决这个问题(具体哪个事务会被中断是很难预测的,而且也不应该依靠这样的预测)。

要注意死锁也可能会作为行级锁的结果而发生(并且因此,它们即使在没有使用显式锁定的情况下也会发生)。考虑如下情况,两个并发事务在修改一个表。第一个事务执行:

UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 11111;

这样就在指定帐号的行上获得了一个行级锁。然后,第二个事务执行:

UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 22222;

UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;

第一个UPDATE语句成功地在指定行上获得了一个行级锁,因此它成功更新了该行。但是第二个UPDATE语句发现它试图更新的行已经被锁住了,因此它等待持有该锁的事务结束。事务二现在就在等待事务一结束,然后再继续执行。现在,事务一执行:

UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;

事务一试图在指定行上获得一个行级锁,但是它得不到:事务二已经持有了这样的锁。所以它要等待事务二完成。因此,事务一被事务二阻塞,而事务二也被事务一阻塞:一个死锁。

瀚高数据库将检测这样的情况并中断其中一个事务。

防止死锁的最好方法通常是保证所有使用一个数据库的应用都以一致的顺序在多个对象上获得锁。在上面的例子里,如果两个事务以同样的顺序更新那些行,那么就不会发生死锁。

我们也应该保证一个事务中在一个对象上获得的第一个锁是该对象需要的最严格的锁模式。

如果我们无法提前验证这些,那么可以通过重试因死锁而中断的事务来即时处理死锁。

只要没有检测到死锁情况,寻求一个表级或行级锁的事务将无限等待冲突锁被释放。这意味着一个应用长时间保持事务开启不是什么好事(例如等待用户输入)。

11.3.5. 咨询锁

瀚高数据库提供了一种方法创建由应用定义其含义的锁。这种锁被称为咨询锁,因为系统并不强迫其使用 — 而是由应用来保证其正确的使用。咨询锁可用于 MVCC 模型不适用的锁定策略。例如,咨询锁的一种常用用法是模拟所谓“平面文件”数据管理系统典型的悲观锁策略。虽然一个存储在表中的标志可以被用于相同目的,但咨询锁更快、可以避免表膨胀并且会由服务器在会话结束时自动清理。

有两种方法在瀚高数据库中获取一个咨询锁:在会话级别或在事务级别。一旦在会话级别获得了咨询锁,它将被保持直到被显式释放或会话结束。不同于标准锁请求,会话级咨询锁请求不尊重事务语义:在一个后来被回滚的事务中得到的锁在回滚后仍然被保持,并且同样即使调用它的事务后来失败一个解锁也是有效的。一个锁在它所属的进程中可以被获取多次;

对于每一个完成的锁请求必须有一个相应的解锁请求,直至锁被真正释放。在另一方面,事务级锁请求的行为更像普通锁请求:在事务结束时会自动释放它们,并且没有显式的解锁操作。这种行为通常比会话级别的行为更方便,因为它使用一个咨询锁的时间更短。对于同一咨询锁标识符的会话级别和事务级别的锁请求按照期望将彼此阻塞。如果一个会话已经持有了一个给定的咨询锁,由它发出的附加请求将总是成功,即使有其他会话在等待该锁;不管现有的锁和新请求是处在会话级别还是事务级别,这种说法都是真的。

和所有瀚高数据库中的锁一样,当前被任何会话所持有的咨询锁的完整列表可以在pg_locks系统视图中找到。

咨询锁和普通锁都被存储在一个共享内存池中,它的尺寸由max_locks_per_transaction和max_connections配置变量定义。必须当心不要耗尽这些内存,否则服务器将不能再授予任何锁。这对服务器可以授予的咨询锁数量设置了一个上限,根据服务器的配置不同,这个限制通常是数万到数十万。

在使用咨询锁方法的特定情况下,特别是查询中涉及显式排序和LIMIT子句时,由于 SQL 表达式被计算的顺序,必须小心控制锁的获取。例如:

SELECT pg_advisory_lock(id) FROM foo WHERE id = 12345; – ok

SELECT pg_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; – danger!

SELECT pg_advisory_lock(q.id) FROM

(

SELECT id FROM foo WHERE id > 12345 LIMIT 100

) q; – ok

在上述查询中,第二种形式是危险的,因为不能保证在锁定函数被执行之前应用LIMIT。这可能导致获得某些应用不期望的锁,并因此在会话结束之前无法释放。从应用的角度来看,这样的锁将被挂起,虽然它们仍然在pg_locks中可见。

提供的操作咨询锁函数在第 8.26.10 节中描述。

11.4. 应用级别的数据完整性检查

对于使用读已提交事务的数据完整性强制业务规则非常困难,因为对每一个语句数据视图都在变化,并且如果一个写冲突发生即使一个单一语句也不能把它自己限制到该语句的快照。

虽然一个可重复读事务在其执行期间有一个稳定的数据视图,在使用MVCC快照进行数据一致性检查时也有一个小问题,它涉及到被称为读/写冲突的东西。如果一个事务写数据并且一个并发事务尝试读相同的数据(不管是在写之前还是之后),它不能看到其他事务的工作。

读取事务看起来是第一个执行的,不管哪个是第一个启动或者哪个是第一个提交。如果就到此为止,则没有问题,但是如果读取者也写入被一个并发事务读取的数据,现在有一个事务好像是已经在前面提到的任何一个事务之前运行。如果看起来最后执行的事务实际上第一个提交,在这些事务的执行顺序图中很容易出现一个环。当这样一个环出现时,完整性检查在没有任何帮助的情况下将不会正确地工作。

正如第 12.2.3 节中提到的,可序列化事务仅仅是可重复读事务增加了对读/写冲突的危险模式的非阻塞监控。当检测到一个可能导致表面的执行顺序中产生环的模式,涉及到的一个事务将被回滚来打破该环。

11.4.1. 用可序列化事务来强制一致性

如果可序列化事务隔离级别被用于所有需要一个一致数据视图的写入和读取,不需要其他的工作来保证一致性。在瀚高数据库中,来自于其他环境的被编写成使用可序列化事务来保证一致性的软件应该“只工作”在这一点上。

当使用这种技术时,如果应用软件通过一个框架来自动重试由于序列化错误而回滚的事务,它将避免为应用程序员带来不必要的负担。把default_transaction_isolation设置为serializable是常用的方法。通过触发器中的事务隔离级别检查来采取某些动作来保证没有其他事务隔离级别被使用(由于疏忽或者为了破坏完整性检查)也是推荐的。

性能建议见第 12.2.3 节。

注意:
这个级别的使用可序列化事务的完整性保护还没有扩展到热备份模式。由于这个原因,那些使用热备份的系统可能想要在主控机上使用可重复读和显式锁定。

11.4.2. 使用显式锁定强制一致性

当可以使用非可序列化写时,要保证一行的当前有效性并保护它不受并发更新的影响,我们必须使用SELECT FOR UPDATE、SELECT FOR SHARE或一个合适的LOCK TABLE 语句(SELECT FOR UPDATE和SELECT FOR SHARE锁只针对并发更新返回行,而LOCK TABLE会锁住整个表)。当从其他环境移植应用到瀚高数据库时需要考虑这些。

关于这些来自其他环境的转换还需要注意的是SELECT FOR UPDATE不保证一个并发事务将不会更新或删除一个被选中的行。要在数据库中这样做,你必须真正地更新该行,即便没有值需要被改变。SELECT FOR UPDATE 临时阻塞其他事务,让它们不能获取该相同的锁或者执行一个会影响被锁定行的UPDATE或DELETE,但是一旦正持有该所锁的事务提交或回滚,一个被阻塞的事务将继续执行冲突操作,除非当锁被持有时一个该行的实际UPDATE被执行。

在非可序列化MVCC环境下,全局有效性检查需要一些额外的考虑。例如,一个银行应用可能会希望检查一个表中的所有扣款总和等于另外一个表中的收款总和,同时两个表还会被更新。比较两个连续的在读已提交模式下不会可靠工作的SELECT sum(…)命令,因为第二个查询很可能会包含没有被第一个查询考虑的事务提交的结果。在一个单一的可重复读事务里进行两个求和则给出在可串行化事务开始之前提交的所有事务产生的准确结果 — 但有人可能会合理地置疑在结果被递交的时候,它们是否仍然相关。如果可重复读事务本身在尝试做一致性检查之前应用了某些变更,那么检查的实用性就更加值得讨论了,因为现在它包含了一些(但不是全部)事务开始后的变化。在这种情况下,一个小心的人可能希望锁住所有需要检查的表,这样才能获得一个无可置疑的当前现状的图像。一个SHARE模式(或者更高)的锁保证在被锁定表中除了当前事务所作的更改之外,没有未提交的更改。

还要注意如果某人正在依赖显式锁定来避免并发更改,那么他应该使用读已提交模式,或者是在可重复读模式里在执行命令之前小心地获取锁。在可重复读事务里获取的锁保证了不会有其它修改该表的事务正在运行,但是如果事务看到的快照在获取锁之前,那么它可能早于表中一些现在已经提交的更改。一个可重复读事务的快照实际上是在它的第一个查询或者数据修改命令(SELECT、INSERT、UPDATE或DELETE)开始的时候冻结的,因此我们可以在快照冻结之前显式地获取锁。

11.5. 提醒

一些 DDL 命令(当前只有TRUNCATE和表重写形式的ALTER TABLE)对于 MVCC 不是安全的。这意味着在截断或者重写提交之后,该表将对并发事务(如果它们使用的快照是在 DDL 命令提交前取得的)呈现出空表的形态。这只对没有在该 DDL命令开始前访问所讨论的表的事务存在问题 — 任何在 DDL 命令开始前访问过该表的事务将持有至少一个 ACCESS SHARE 表锁,这将阻塞该 DDL 命令直到该事务完成。因此这些命令对于目标表上的连续查询将不会造成任何明显的表内容不一致,但是它们可能导致目标表内容和数据库中其他表内容之间的不一致。

对于可序列化事务隔离级别的支持还没有被加入到热备复制目标中。当前在热备模式中支持的最严格的隔离级别是可重复读。虽然在主控机上用可序列化事务执行所有持久化数据库写入将确保所有后备机将最终达到一个一致的状态,但是运行在后备机上的一个可重复读事务有时可能会看到一个短暂的、与主控机上事务的任何串行执行都不一致的状态。

11.6. 锁定和索引

尽管瀚高数据库提供对表数据访问的非阻塞读/写,但并非瀚高数据库中实现的每一个索引访问方法当前都能够提供非阻塞读/写访问。不同的索引类型按照下面方法操作:

B-tree、GiST和SP-GiST索引

短期的页面级共享/排他锁被用于读/写访问。每个索引行被取得或被插入后立即释放锁。这些索引类型提供了无死锁情况的最高并发性。

Hash索引

Hash 桶级别的共享/排他锁被用于读/写访问。锁在整个 Hash 桶处理完成后释放。Hash 桶级锁比索引级的锁提供了更好的并发性但是可能产生死锁,因为锁持有的时间比一次索引操作时间长。

GIN索引

短期的页面级共享/排他锁被用于读/写访问。锁在索引行被插入/抓取后立即释放。但要注意的是一个 GIN 索引值的插入通常导致对每行产生几个索引键的插入,因此 GIN 可能为了插入一个单一值而做大量的工作。

目前,B-tree 索引为并发应用提供了最好的性能。因为它还有比 Hash 索引更多的特性,在那些需要对标量数据进行索引的并发应用中,我们建议使用 B-tree 索引类型。在处理非标量类型数据的时候,B-tree 就没什么用了,应该使用 GiST、SP-GiST 或 GIN 索引替代。