PostgreSQL 提供各种锁模式来控制表中数据的并发访问。这些模式可用于应用程序控制锁定,用于在MVCC未提供所需行为的情况下。此外,大多数 PostgreSQL 命令自动获取适当模式的锁,以确保在命令执行期间不会以不兼容的方式删除或修改引用的表。(例如,TRUNCATE
无法与同一表上的其他操作安全地并发执行,因此它在表上获取一个 ACCESS EXCLUSIVE
锁来强制执行。)
要检查数据库服务器中当前未执行的锁的列表,请使用 pg_locks
系统视图。有关监视锁管理器子系统状态的详细信息,请参阅 第 27 章。
下面的列表显示了可用的锁模式及 PostgreSQL 自动在其中使用它们的上下文。还可以使用命令 LOCK 显式获取这些锁中的任何一个。请记住,所有这些锁模式都是表级锁,即使名称包含单词 “行”;锁模式的名称是历史性的。某种程度上,名称反映了每个锁模式的典型用法——但语义都是相同的。一种锁模式与另一种锁模式之间的唯一实质性区别在于每种锁模式与之冲突的锁模式集(请参阅 表 13.2)。两个事务无法同时在同一张表上持有冲突模式的锁。(但是,事务永远不会与自身冲突。例如,它可以获取 ACCESS EXCLUSIVE
锁,然后在同一张表上获取 ACCESS SHARE
锁。)非冲突锁模式可以由许多事务同时持有。特别注意,某些锁模式是自冲突的(例如,一次只能由一个事务持有 ACCESS EXCLUSIVE
锁),而其他锁模式则不是自冲突的(例如,ACCESS SHARE
锁可以由多个事务持有)。
表级锁定模式
ACCESS SHARE
(AccessShareLock
)仅与 ACCESS EXCLUSIVE
锁模式冲突。
SELECT
命令获得对引用表的此模式的锁。一般而言,任何仅读取某表而不会对其进行修改的查询都会获得此锁定模式。
ROW SHARE
(RowShareLock
)与 EXCLUSIVE
和ACCESS EXCLUSIVE
锁定模式冲突。
SELECT
命令获得对指定了FOR UPDATE
、FOR NO KEY UPDATE
、FOR SHARE
或 FOR KEY SHARE
选项之一(除以 ACCESS SHARE
锁之外,其他任何表均由任何显式 FOR ...
锁定选项引用)的所有表的此模式的锁。
ROW EXCLUSIVE
(RowExclusiveLock
)与 SHARE
、SHARE ROW EXCLUSIVE
、EXCLUSIVE
和ACCESS EXCLUSIVE
锁定模式冲突。
命令 UPDATE
、DELETE
、INSERT
和 MERGE
获得对目标表的此锁定模式(除以 ACCESS SHARE
锁之外,其他任何引用表均由任何显式 FOR ...
锁定选项引用)。一般而言,此锁定模式将通过任何修改表中数据的命令获得。
SHARE UPDATE EXCLUSIVE
(ShareUpdateExclusiveLock
)与 SHARE UPDATE EXCLUSIVE
、SHARE
、SHARE ROW EXCLUSIVE
、EXCLUSIVE
和ACCESS EXCLUSIVE
锁定模式冲突。此模式保护某表免遭并发架构变更和VACUUM
运行。
由 VACUUM
(没有FULL
)、ANALYZE
、CREATE INDEX CONCURRENTLY
、CREATE STATISTICS
、COMMENT ON
、REINDEX CONCURRENTLY
,以及某些ALTER INDEX
和ALTER TABLE
变体获取(有关完整详细信息,请参阅这些命令的文档)。
SHARE
(ShareLock
)与 ROW EXCLUSIVE
、SHARE UPDATE EXCLUSIVE
、SHARE ROW EXCLUSIVE
、EXCLUSIVE
和ACCESS EXCLUSIVE
锁定模式冲突。此模式保护某表免遭并发数据变更。
由 CREATE INDEX
(没有CONCURRENTLY
)获取。
SHARE ROW EXCLUSIVE
(ShareRowExclusiveLock
)与 ROW EXCLUSIVE
、SHARE UPDATE EXCLUSIVE
、SHARE
、SHARE ROW EXCLUSIVE
、EXCLUSIVE
和 ACCESS EXCLUSIVE
锁模式冲突。此模式保护表格免受并发数据更改影响,并且是自排斥性的,因此一次只能由一个会话持有它。
通过 CREATE TRIGGER
和某些形式的 ALTER TABLE
获得。
EXCLUSIVE
(ExclusiveLock
)与 ROW SHARE
、ROW EXCLUSIVE
、SHARE UPDATE EXCLUSIVE
、SHARE
、SHARE ROW EXCLUSIVE
、EXCLUSIVE
和 ACCESS EXCLUSIVE
锁模式冲突。此模式只允许并发 ACCESS SHARE
锁,即从表格读取只能与持有此锁模式的事务并行进行。
通过 REFRESH MATERIALIZED VIEW CONCURRENTLY
获得。
ACCESS EXCLUSIVE
(AccessExclusiveLock
)与所有模式的锁冲突(ACCESS SHARE
、ROW SHARE
、ROW EXCLUSIVE
、SHARE UPDATE EXCLUSIVE
、SHARE
、SHARE ROW EXCLUSIVE
、EXCLUSIVE
和 ACCESS EXCLUSIVE
)。此模式保证持有者是唯一以任何方式访问该表格的事务。
由 DROP TABLE
、TRUNCATE
、REINDEX
、CLUSTER
、VACUUM FULL
和 REFRESH MATERIALIZED VIEW
(无 CONCURRENTLY
)命令获得。许多形式的 ALTER INDEX
和 ALTER TABLE
也获得此级别的锁。这也是未明确指定模式的 LOCK TABLE
语句的默认锁模式。
只有 ACCESS EXCLUSIVE
锁才会阻止不带有 FOR UPDATE/SHARE
的 SELECT
语句。
一旦获得,通常会将锁持有到事务的末尾。但是,如果在建立保存点后获得锁,则在保存点回滚后立即释放锁。这与 ROLLBACK
取消自保存点以来所有命令的效果的原则是一致的。对于 PL/pgSQL 异常块中获得的锁也是如此:从该块中转义错误将释放其中获得的锁。
表 13.2. 冲突锁模式
请求的锁模式 | 现有锁模式 | |||||||
---|---|---|---|---|---|---|---|---|
ACCESS SHARE |
ROW SHARE |
ROW EXCL. |
SHARE UPDATE EXCL. |
SHARE |
SHARE ROW EXCL. |
EXCL. |
ACCESS EXCL. |
|
ACCESS SHARE |
X | |||||||
ROW SHARE |
X | X | ||||||
ROW EXCL. |
X | X | X | X | ||||
SHARE UPDATE EXCL. |
X | X | X | X | X | |||
SHARE |
X | X | X | X | X | |||
SHARE ROW EXCL. |
X | X | X | X | X | X | ||
EXCL. |
X | X | X | X | X | X | X | |
ACCESS EXCL. |
X | X | X | X | X | X | X | X |
除了表级锁外,还有行级锁,它们如下所示,
PostgreSQL
自动使用它们的环境。请参阅表 13.3以获取有关行级锁冲突的完整表。请注意,即使在不同的子事务中,事务也可以对同一行保留冲突锁;但在那之外,两个事务绝不会对同一行保留冲突锁。行级锁不会影响数据查询;它们仅会阻止对同一行的写入者和锁持有者。行级锁会在事务结束时或在回滚保存点时释放,就像表级锁一样。行级锁模式
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 节。
FOR UPDATE
锁模式也由行上的任意DELETE
以及修改某些列的值的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
。
PostgreSQL 不会记住内存中关于修改行的任何信息,因此对于一次性锁定的行数没有限制。然而,锁定一行可能会导致磁盘写入,例如 SELECT FOR UPDATE
修改选定的行并将其标记为已锁定,因此会导致磁盘写入。
表 13.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 |
除了表锁和行锁,还使用页级共享/独占锁来控制对共享缓冲池中表页的读/写访问。这些锁在行被获取或更新后会立即释放。应用程序开发人员通常不需要关注页级锁,但为了完整性,此处提到了页级锁。
使用显式锁定会增加 死锁 的可能性,其中两个(或更多)事务各自持有另外的事务想要获取的锁。例如,如果事务 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会检测到这种情况,并中止其中一个事务。
防止死锁的最有效办法通常是确保使用数据库的所有应用程序总是以某种确定的顺序获取多个对象的锁,从而避免死锁。在上面的示例中,如果这两个事务以相同的顺序更新行,那将不会出现死锁。还应该确保在事务中获取对象的第一个锁是最严格模式的,它将是为那个对象必需的。如果无法提前验证这一点,那么可以动态地处理死锁,办法是重新尝试中止于死锁的事务。
只要没有检测出死锁情况,那么寻求表级锁或行级锁的事务将无限期地等待冲突的锁释放。这意味着应用程序在长时间内保留事务打开(例如,在等待用户输入的过程中)是个错误的想法。
PostgreSQL 提供一种创建具有应用程序定义意义的锁定的方式。它们被称为咨询锁定,这是因为系统不强制使用它们——由应用程序来正确使用它们。咨询锁定对于锁定策略很有用,而锁定策略不适用于 MVCC 模型。例如,咨询锁定的一种常见用法是模拟所谓的“平面文件”数据管理系统中常见的悲观锁定策略。虽然可以将存储在表中的标志用于相同目的,但咨询锁定更快,可以避免表急剧增大,并且会在会话结束后由服务器自动清理。
有两种方法可在 PostgreSQL 中获取咨询锁定:会话级别或事务级别。一旦在会话级别获取,将会持有咨询锁定,直至明确释放或会话结束为止。与标准锁定请求不同,会话级别咨询锁定请求并不遵守事务语义:在稍后回滚的事务期间获取的锁定即使在回滚后仍然会持有,并且同样地,即使调用事务稍后失败,解锁仍然有效。拥有进程可以多次获取锁定;对于每个已完成的锁定请求,在锁定实际释放之前都必须有相应的解锁请求。另一方面,事务级别锁定请求的行为更像常规锁定请求:它们会在事务结束时自动释放,并且没有显式的解锁操作。对于短期使用咨询锁定而言,此行为通常比会话级别行为更方便。同一咨询锁定标识符的会话级别和事务级别锁定请求会以预期方式互相阻塞。如果会话已持有给定的咨询锁定,它发起的额外请求将始终成功,即使其他会话正在等待锁定也是如此;无论现有的锁定持有和新请求是在会话级别还是事务级别,此语句都是正确的。
与 PostgreSQL 中的所有锁定一样,任何会话当前持有的咨询锁定的完整列表都可以在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
中查看。
用于操纵咨询锁的函数在 第 9.28.10 节 中进行了说明。