Skip to content

13.3. 显式锁定#

13.3.1. 表级锁定
13.3.2. 行级锁定
13.3.3. 页级锁定
13.3.4. 死锁
13.3.5. 建议锁定

PostgreSQL提供了各种锁定模式来控制对表中数据的并发访问。这些模式可用于在MVCC无法提供所需行为的情况下进行应用程序控制的锁定。此外,大多数PostgreSQL命令会自动获取适当模式的锁定,以确保在命令执行期间不会以不兼容的方式删除或修改引用的表。(例如,TRUNCATE无法与同一表上的其他操作安全地并发执行,因此它会获取表上的ACCESS EXCLUSIVE锁定以强制执行此操作。)

要检查数据库服务器中当前未完成锁定的列表,请使用pg_locks系统视图。有关监视锁定管理器子系统状态的更多信息,请参阅第 28 章

13.3.1. 表级锁定#

以下列表显示了PostgreSQL自动使用的可用锁模式及其上下文。您还可以使用命令LOCK明确获取任何这些锁。请记住,所有这些锁模式都是表级锁,即使名称包含单词“row”;锁模式的名称具有历史意义。在某种程度上,名称反映了每个锁模式的典型用法——但语义都是相同的。一种锁模式与另一种锁模式之间的唯一真正区别是它们各自冲突的锁模式集(请参见Table 13.2)。两个事务不能同时在同一张表上持有冲突模式的锁。(但是,一个事务永远不会与自身冲突。例如,它可能会获取ACCESS EXCLUSIVE锁,然后在同一张表上获取ACCESS SHARE锁。)非冲突锁模式可以由许多事务同时持有。特别注意,一些锁模式是自冲突的(例如,ACCESS EXCLUSIVE锁不能由多个事务同时持有),而另一些则不是自冲突的(例如,ACCESS SHARE锁可以由多个事务持有)。

表级锁模式

ACCESS SHARE (AccessShareLock)

仅与 ACCESS EXCLUSIVE 锁模式冲突。

SELECT 命令在引用的表上获取此模式的锁。一般来说,任何仅 读取 表且不修改它的查询都将获取此锁模式。

ROW SHARE (RowShareLock)

EXCLUSIVEACCESS EXCLUSIVE 锁模式冲突。

SELECT 命令在指定了 FOR UPDATEFOR NO KEY UPDATEFOR SHAREFOR KEY SHARE 选项之一的所有表上获取此模式的锁(除了在任何其他表上获取 ACCESS SHARE 锁,而这些表未引用任何显式 FOR ... 锁定选项)。

ROW EXCLUSIVE (RowExclusiveLock)

SHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE 锁模式冲突。

命令 UPDATEDELETEINSERTMERGE 在目标表上获取此锁模式(除了对任何其他引用的表上的 ACCESS SHARE 锁)。通常,此锁模式将由任何在表中 修改数据 的命令获取。

SHARE UPDATE EXCLUSIVE (ShareUpdateExclusiveLock)

SHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE 锁模式冲突。此模式保护表免受并发架构更改和 VACUUM 运行的影响。

VACUUM(不带 FULL)、ANALYZECREATE INDEX CONCURRENTLYCREATE STATISTICSCOMMENT ONREINDEX CONCURRENTLY 以及某些 ALTER INDEXALTER TABLE 变体获取(有关详细信息,请参阅这些命令的文档)。

SHARE (ShareLock)

ROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE 锁模式冲突。此模式保护表免受并发数据更改的影响。

CREATE INDEX(不带 CONCURRENTLY)获取。

SHARE ROW EXCLUSIVE (ShareRowExclusiveLock)

ROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE 锁模式冲突。此模式保护表免受并发数据更改的影响,并且是自排他的,因此一次只能有一个会话持有它。

CREATE TRIGGER 和某些形式的 ALTER TABLE 获取。

EXCLUSIVE (ExclusiveLock)

ROW SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE 锁模式冲突。此模式仅允许并发 ACCESS SHARE 锁,即,仅允许与持有此锁模式的事务并行读取表。

REFRESH MATERIALIZED VIEW CONCURRENTLY 获取。

ACCESS EXCLUSIVE (AccessExclusiveLock)

与所有模式的锁冲突 (ACCESS SHAREROW SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE)。此模式保证持有者是唯一以任何方式访问表的的事务。

DROP TABLETRUNCATEREINDEXCLUSTERVACUUM FULLREFRESH MATERIALIZED VIEW(不带 CONCURRENTLY)命令获取。许多形式的 ALTER INDEXALTER TABLE 也在此级别获取锁。这也是未明确指定模式的 LOCK TABLE 语句的默认锁模式。

提示

只有ACCESS EXCLUSIVE锁会阻止SELECT(不带FOR UPDATE/SHARE)语句。

一旦获取锁,通常会一直持有到事务结束。但是,如果在建立保存点后获取锁,则在回滚到保存点时立即释放锁。这与ROLLBACK会取消自保存点以来的所有命令效果的原则是一致的。对于在PL/pgSQL异常块中获取的锁也是如此:从块中转义错误会释放其中获取的锁。

表 13.2. 冲突锁模式

请求的锁模式现有锁模式
ACCESS SHARE行共享行排他。共享更新排他。共享共享行排他。排他。访问排他。
ACCESS SHAREX
行共享XX
行排他。XXXX
共享更新排他。XXXXX
共享XXXXX
共享行排他。XXXXXX
排他。XXXXXXX
访问排他。XXXXXXXX

13.3.2. 行级锁#

除了表级锁之外,还有行级锁,如下列出,其中列出了PostgreSQL自动使用它们的上下文。有关行级锁冲突的完整表,请参见表 13.3。请注意,一个事务可以在同一行上持有冲突锁,即使在不同的子事务中也是如此;但除此之外,两个事务绝不会在同一行上持有冲突锁。行级锁不会影响数据查询;它们只阻止对同一行进行写入和加锁。行级锁在事务结束时或保存点回滚期间释放,就像表级锁一样。

行级锁模式

FOR UPDATE

FOR UPDATE 导致 SELECT 语句检索的行被锁定,就像要进行更新一样。这可以防止其他事务在当前事务结束之前锁定、修改或删除它们。也就是说,尝试对这些行执行 UPDATEDELETESELECT FOR UPDATESELECT FOR NO KEY UPDATESELECT FOR SHARESELECT FOR KEY SHARE 的其他事务将被阻止,直到当前事务结束;相反,SELECT FOR UPDATE 将等待在同一行上对这些命令中的任何一个运行的并发事务,然后锁定并返回更新的行(如果没有该行,则不返回)。但是,在 REPEATABLE READSERIALIZABLE 事务中,如果要锁定的行自事务开始以来已更改,则会引发错误。有关进一步的讨论,请参见 第 13.4 节

任何对行的 DELETE 也会获取 FOR UPDATE 锁模式,而修改某些列值的 UPDATE 也会获取。目前,为 UPDATE 案例考虑的列集是在外键中可以使用它们的唯一索引(因此不考虑部分索引和表达式索引),但这可能会在将来发生变化。

不更新任何键

行为类似于 FOR UPDATE,但获取的锁更弱:此锁不会阻止尝试获取同一行锁的 SELECT FOR KEY SHARE 命令。任何不获取 FOR UPDATE 锁的 UPDATE 也会获取此锁模式。

共享

行为类似于 FOR NO KEY UPDATE,但获取的是共享锁,而不是对每行检索到的独占锁。共享锁阻止其他事务对这些行执行 UPDATEDELETESELECT FOR UPDATESELECT FOR NO KEY UPDATE,但不会阻止它们执行 SELECT FOR SHARESELECT FOR KEY SHARE

键共享

行为类似于 FOR SHARE,但锁更弱:SELECT FOR UPDATE 被阻止,但 SELECT FOR NO KEY UPDATE 不被阻止。键共享锁阻止其他事务执行 DELETE 或任何更改键值的 UPDATE,但不会阻止其他 UPDATE,也不会阻止 SELECT FOR NO KEY UPDATESELECT FOR SHARESELECT FOR KEY SHARE

PostgreSQL不会记住内存中修改行有关的任何信息,因此一次锁定的行数没有限制。但是,锁定一行可能会导致磁盘写入,例如,SELECT FOR UPDATE修改选定行以标记它们已锁定,因此将导致磁盘写入。

表 13.3. 冲突的行级锁

请求的锁模式当前锁模式
键共享共享不更新任何键FOR UPDATE
键共享X
共享XX
不更新任何键XXX
FOR UPDATEXXXX

13.3.3. 页级锁#

除了表锁和行锁之外,页级共享/独占锁还用于控制共享缓冲池中表页的读/写访问。这些锁在获取或更新行后立即释放。应用程序开发人员通常不必关心页级锁,但在此处提及它们是为了完整性。

13.3.4. 死锁#

使用显式锁定会增加死锁的可能性,其中两个(或更多)事务各自持有对方需要的锁。例如,如果事务 1 获取表 A 的独占锁,然后尝试获取表 B 的独占锁,而事务 2 已独占锁定表 B,现在想要表 A 的独占锁,那么这两个事务都无法继续进行。PostgreSQL会自动检测死锁情况并通过中止涉及的事务之一来解决这些情况,从而允许其他事务完成。(很难预测哪个事务将被中止,也不应依赖于此。)

请注意,死锁也可能由于行级锁而发生(因此,即使没有使用显式锁,它们也可能发生)。考虑两个并发事务修改表的情况。第一个事务执行

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;

事务一尝试获取指定行上的行级锁,但它无法获取:事务二已经持有这样的锁。因此,它等待事务二完成。因此,事务一被事务二阻塞,事务二被事务一阻塞:死锁条件。PostgreSQL将检测到这种情况并中止其中一个事务。

防止死锁的最佳方法通常是避免它们,方法是确保使用数据库的所有应用程序以一致的顺序获取多个对象上的锁。在上面的示例中,如果两个事务都以相同的顺序更新行,则不会发生死锁。还应该确保在事务中获取的第一个锁是该对象将需要的最严格模式。如果无法预先验证这一点,则可以通过重试因死锁而中止的事务来动态处理死锁。

只要未检测到死锁情况,寻求表级或行级锁的事务将无限期等待释放冲突锁。这意味着应用程序长时间保持事务处于打开状态(例如,在等待用户输入时)是一个坏主意。

13.3.5. 咨询锁#

PostgreSQL提供了一种创建具有应用程序定义含义的锁的方法。这些锁称为咨询锁,因为系统并不强制使用它们——应用程序必须正确使用它们。咨询锁对于不适合 MVCC 模型的锁定策略很有用。例如,咨询锁的常见用途是模拟所谓的“平面文件”数据管理系统中典型的悲观锁定策略。虽然存储在表中的标志可以用于相同目的,但咨询锁更快,避免表膨胀,并且在会话结束时由服务器自动清理。

有两种方法可以在PostgreSQL中获取咨询锁:在会话级别或在事务级别。一旦在会话级别获取,咨询锁将被持有,直到明确释放或会话结束。与标准锁请求不同,会话级咨询锁请求不遵守事务语义:在稍后回滚的事务期间获取的锁在回滚后仍将被持有,同样,即使调用事务稍后失败,解锁也是有效的。一个锁可以被其拥有进程多次获取;对于每个已完成的锁请求,在实际释放锁之前必须有一个相应的解锁请求。另一方面,事务级锁请求的行为更像是常规锁请求:它们在事务结束时自动释放,并且没有明确的解锁操作。对于咨询锁的短期使用,此行为通常比会话级行为更方便。针对同一咨询锁标识符的会话级和事务级锁请求将以预期的方式相互阻塞。如果会话已经持有给定的咨询锁,则它发出的其他请求将始终成功,即使其他会话正在等待锁也是如此;无论现有锁持有和新请求是在会话级别还是事务级别,此语句都是正确的。

与PostgreSQL中的所有锁一样,任何会话当前持有的咨询锁的完整列表都可以在pg_locks系统视图中找到。

咨询锁和常规锁都存储在共享内存池中,其大小由配置变量max_locks_per_transactionmax_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中查看。

用于操作咨询锁的函数在第 9.27.10 节中进行了描述。