PostgreSQL 提供了多种锁模式,用于控制对表中数据的并发访问。 在 MVCC 无法给出期望行为的场景中,这些模式可用于由应用自行控制的加锁。 此外,大多数 PostgreSQL 命令也会自动获取适当模式的锁, 以确保在命令执行期间,被引用的表不会以不兼容的方式被删除或修改。 (例如,TRUNCATE 无法安全地与同一张表上的其他操作并发执行, 因此它会在该表上获取 ACCESS EXCLUSIVE 锁来强制实现这一点。)
要查看数据库服务器中当前尚未释放的锁列表,可以使用 pg_locks 系统视图。有关监控锁管理器子系统状态的更多信息,请参见 Chapter 27。
下面的列表给出了可用的锁模式,以及它们在 PostgreSQL 中被自动使用的场景。 你也可以通过命令 LOCK 显式获取其中任意一种锁。 请记住,这些锁模式全部都是表级锁,即使名称中包含 “row” 一词;这些名称只是历史遗留。 在某种程度上,这些名称反映了各锁模式的典型用途 — 但它们的语义是完全相同的。 一个锁模式与另一个锁模式真正的区别,只在于它与哪些锁模式冲突(见 Table 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 选项时,会在该表上获取这种锁模式 (此外,对于任何其他被引用但没有显式 FOR ... 加锁选项的表, 则会获取 ACCESS SHARE 锁)。
ROW EXCLUSIVE (RowExclusiveLock) #与SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。
UPDATE、DELETE 和 INSERT 命令会在目标表上获取这种锁模式(此外,对任何其他被引用的表还会获取 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 变体会获取这种锁 (有关详细信息,请参阅这些命令的文档)。
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 和不带 CONCURRENTLY 的 REFRESH MATERIALIZED VIEW 命令获取。 多种形式的 ALTER INDEX 和 ALTER TABLE 也会获取这一层级的锁。这也是未显式指定模式的 LOCK TABLE 语句的默认锁模式。
只有 ACCESS EXCLUSIVE 锁才会阻塞 SELECT(不带 FOR UPDATE/SHARE)语句。
锁一旦被获取,通常会一直持有到事务结束。但是,如果锁是在建立保存点之后才获取的,那么在回滚到该保存点时,这个锁会立即释放。这与 ROLLBACK 会取消保存点之后所有命令效果的原则是一致的。相同的原则也适用于在 PL/pgSQL 异常块中获取的锁:从该块中因错误跳出时,会释放在块中获取的锁。
Table 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 会在哪些场景下自动使用它们。行级锁的完整冲突表见 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 事务中,如果要锁定的行自事务开始以来已经发生变化,就会抛出错误。进一步讨论见 Section 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 会修改被选中的行以标记它们已被锁定,因此会产生磁盘写入。
Table 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 中看到。
用于操作咨询锁的函数见 Section 9.27.10。
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。