PostgreSQL:显式锁

13.3 显式锁

PostgreSQL 提供多种锁模式,用于控制对表中数据的并发访问。当 MVCC 无法提供所需行为时,应用可以使用这些模式自行控制加锁。此外,大多数 PostgreSQL 命令会自动获取适当模式的锁,保证命令执行期间,所引用的表不会被删除或以不兼容的方式修改。例如,TRUNCATE 无法与同一表上的其他操作安全地并发执行,因此会获取该表的 ACCESS EXCLUSIVE 锁来保证这一点。

要查看数据库服务器中当前尚未释放的锁,请使用 pg_locks 系统视图。有关监视锁管理器子系统状态的更多信息,请参阅第 27 章。(pg_locks · Chapter 27)

13.3.1 表级锁

下面列出可用锁模式,以及 PostgreSQL 自动使用它们的场景。你也可以通过 LOCK 命令显式获取任意一种锁。请记住,即使名称包含 row(行),这些模式也全部是表级锁;名称有历史原因。名称在一定程度上反映了典型用途,但它们的基本语义相同。不同模式真正的区别,是各自与哪些锁模式冲突(见表 13.2)。两个事务不能同时在同一表上持有相互冲突的锁。不过,事务不会与自身冲突:例如,它可以先获取 ACCESS EXCLUSIVE 锁,随后再获取同一表的 ACCESS SHARE 锁。多个事务可以同时持有互不冲突的锁。尤其要注意,有些模式会与自身冲突,例如同一时刻只能有一个事务持有 ACCESS EXCLUSIVE;有些则不会,例如多个事务可以持有 ACCESS SHARE。(LOCK · Table 13.2)

表级锁模式

ACCESS SHARE (AccessShareLock)

仅与 ACCESS EXCLUSIVE 锁模式冲突。

SELECT 命令会在所引用的表上获取此模式的锁。一般而言,只读取表而不修改表的查询都会获取这种锁。

ROW SHARE (RowShareLock)

与 EXCLUSIVE 和 ACCESS EXCLUSIVE 锁模式冲突。

SELECT 命令会在指定了 FOR UPDATE、FOR NO KEY UPDATE、FOR SHARE 或 FOR KEY SHARE 任一选项的所有表上获取这种锁;对于其他被引用但未显式指定 FOR … 加锁选项的表,则另外获取 ACCESS SHARE 锁。

ROW EXCLUSIVE (RowExclusiveLock)

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

UPDATE、DELETE、INSERT 和 MERGE 命令会在目标表上获取此模式的锁,同时在其他被引用的表上获取 ACCESS SHARE 锁。通常,修改表中数据的命令都会获取这种锁。

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 变体会获取这种锁。完整细节请参阅这些命令的文档。(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 会获取这种锁。(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

13.3.2 行级锁

除了表级锁,还有行级锁。下面列出行级锁及 PostgreSQL 自动使用它们的场景。完整的行级锁冲突表见表 13.3。注意,一个事务可以在同一行上持有相互冲突的锁,即便这些锁属于不同子事务;除此之外,两个事务绝不能在同一行上持有相互冲突的锁。行级锁不影响数据查询,只阻塞同一行上的写入者和加锁者。与表级锁一样,行级锁在事务结束或保存点回滚时释放。(Table 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 节。(Section 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。键共享锁会阻止其他事务删除行或执行修改键值的 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

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 发现要更新的行已经被锁定,因此等待持锁事务完成。事务 2 现在必须等待事务 1 完成,才能继续执行。这时事务 1 执行:

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

事务 1 尝试获取指定行的行级锁,却无法成功,因为事务 2 已经持有该锁。因此它等待事务 2 完成。于是事务 1 被事务 2 阻塞,事务 2 又被事务 1 阻塞,形成死锁。PostgreSQL 会检测到这一情况,并中止其中一个事务。

一般而言,防止死锁的最佳办法,是确保使用数据库的所有应用始终以一致顺序获取多个对象的锁。在上例中,如果两个事务按相同顺序更新行,就不会发生死锁。此外,应确保事务在某个对象上获取的第一个锁,就是该对象所需的最严格模式。如果无法提前验证这些条件,也可以在运行中重试因死锁而中止的事务,以处理死锁。

只要没有检测到死锁,尝试获取表级锁或行级锁的事务就会无限期等待冲突锁释放。因此,应用长时间保持事务打开,例如等待用户输入,是不妥的做法。

13.3.5 咨询锁

PostgreSQL 支持创建由应用定义含义的锁,称为咨询锁(advisory locks)。系统不会强制要求使用这些锁,正确使用完全由应用负责。当某种加锁策略难以适配 MVCC 模型时,咨询锁会很有用。例如,它们常被用于模拟所谓“平面文件”数据管理系统中典型的悲观锁策略。虽然也可以在表中保存标志实现同样目的,但咨询锁更快,能避免表膨胀,并且会在会话结束时由服务器自动清理。

PostgreSQL 有两种获取咨询锁的方式:会话级与事务级。会话级咨询锁一旦获取,就会一直持有,直到显式释放或会话结束。与标准加锁请求不同,会话级咨询锁不遵循事务语义:如果事务中获取了锁,之后事务回滚,回滚后锁仍然存在;同样,即使调用解锁的事务后来失败,解锁也仍然有效。持锁进程可以多次获取同一把锁;实际释放前,每次完成的加锁请求都必须对应一次解锁请求。事务级咨询锁则更像普通锁:事务结束时自动释放,没有显式解锁操作。对于短期使用咨询锁的场景,这通常比会话级行为更方便。针对同一个咨询锁标识符,会话级和事务级请求会按预期相互阻塞。如果一个会话已经持有某个咨询锁,它再次请求该锁总会成功,即使其他会话也在等待。无论已有锁及新请求属于会话级还是事务级,这一规则都成立。

与 PostgreSQL 中所有锁一样,可以通过 pg_locks 系统视图查看当前任何会话持有的全部咨询锁。(pg_locks)

咨询锁和普通锁都存储在共享内存池中,其大小由 max_locks_per_transaction 和 max_connections 配置变量决定。必须避免耗尽这部分内存,否则服务器将无法再授予任何锁。这也限制了服务器能够授予的咨询锁数量;具体取决于配置,通常在数万至数十万之间。(max_locks_per_transaction · max_connections)

使用咨询锁方法时,在某些情形下必须注意 SQL 表达式的求值顺序,以控制实际获取的锁,尤其是在包含显式排序和 LIMIT 子句的查询中。例如:

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 节。(Section 9.28.10)


来源:PostgreSQL:显式锁。PostgreSQL Global Development Group,PostgreSQL License。 本文依据本批次转载授权译为中文,原始代码与示例输出保留。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容