heavyweight locks 分析:基础
本页目录
重量级锁,包括关系级锁和行级锁,是由查询获取的,并且始终会保持到该查询所属事务结束为止。因此,重要原则是:一旦获取了锁,便不会释放,直到 COMMIT 或 ROLLBACK。
文档:Explicit locking。关于该文档有几点需要注意:
- 标题 "Explicit Locking" 可能会让人误解 — 它实际上描述了可以通过任何语句隐式获取的锁级别,而不仅仅是通过
LOCK显式获取的锁。 - 该页面还包含一张非常有用的表格"冲突锁模式",帮助理解某些锁因冲突无法获取,并且需要等待持有此类锁的事务完成,才能释放这些"阻塞"锁。这篇文章有一张替代表格,也可能会有帮助:PostgreSQL rocks, except when it blocks: Understanding locks
- 还可能在术语上有些混淆。当讨论"表级锁"时,我们实际上可能是指"关系级锁"。此处,术语"关系"有更广泛的含义:表、索引、视图、物化视图。
我们如何查看某个特定事务/会话中已经获取的锁 (granted),或正在尝试但尚未获取的锁 (pending)?
为此,Postgres 提供了一个系统视图:pg_locks
重要的规则是:分析应在一个单独的会话中进行,以避免"观察者效应" (即由分析本身获取的锁)。
例如,考虑以下表:
nik=# \d t1
Table "public.t1"
Column | Type | Collation | Nullable | Default
--------+--------+-----------+----------+---------
c1 | bigint | | |
Indexes:
"t1_c1_idx" btree (c1)
"t1_c1_idx1" btree (c1)
"t1_c1_idx10" btree (c1)
"t1_c1_idx11" btree (c1)
"t1_c1_idx12" btree (c1)
"t1_c1_idx13" btree (c1)
"t1_c1_idx14" btree (c1)
"t1_c1_idx15" btree (c1)
"t1_c1_idx16" btree (c1)
"t1_c1_idx17" btree (c1)
"t1_c1_idx18" btree (c1)
"t1_c1_idx19" btree (c1)
"t1_c1_idx2" btree (c1)
"t1_c1_idx20" btree (c1)
"t1_c1_idx3" btree (c1)
"t1_c1_idx4" btree (c1)
"t1_c1_idx5" btree (c1)
"t1_c1_idx6" btree (c1)
"t1_c1_idx7" btree (c1)
"t1_c1_idx8" btree (c1)
"t1_c1_idx9" btree (c1)
在第一个 (主) 会话中:
nik=# begin;
BEGIN
nik=*# select from t1 limit 0;
--
(0 rows)
我们打开了一个事务,执行了 SELECT from t1 — 请求 0 行和 0 列,但这已足够获取关系级锁。要查看这些锁,我们首先需要获取第一个会话的 PID,运行以下命令:
nik=*# select pg_backend_pid();
pg_backend_pid
----------------
73053
(1 row)
然后,在另一个会话中:
nik=# select relation, relation::regclass as relname, mode, granted, fastpath
from pg_locks
where pid = 73053 and locktype = 'relation'
order by relname;
relation | relname | mode | granted | fastpath
----------+-------------+-----------------+---------+----------
74298 | t1 | AccessShareLock | t | t
74301 | t1_c1_idx | AccessShareLock | t | t
74318 | t1_c1_idx1 | AccessShareLock | t | t
74319 | t1_c1_idx2 | AccessShareLock | t | t
74320 | t1_c1_idx3 | AccessShareLock | t | t
74321 | t1_c1_idx4 | AccessShareLock | t | t
74322 | t1_c1_idx5 | AccessShareLock | t | t
74323 | t1_c1_idx6 | AccessShareLock | t | t
74324 | t1_c1_idx7 | AccessShareLock | t | t
74325 | t1_c1_idx8 | AccessShareLock | t | t
74326 | t1_c1_idx9 | AccessShareLock | t | t
74327 | t1_c1_idx10 | AccessShareLock | t | t
74328 | t1_c1_idx11 | AccessShareLock | t | t
74337 | t1_c1_idx12 | AccessShareLock | t | t
74338 | t1_c1_idx13 | AccessShareLock | t | t
74339 | t1_c1_idx14 | AccessShareLock | t | t
74345 | t1_c1_idx20 | AccessShareLock | t | f
74346 | t1_c1_idx15 | AccessShareLock | t | f
74347 | t1_c1_idx16 | AccessShareLock | t | f
74348 | t1_c1_idx17 | AccessShareLock | t | f
74349 | t1_c1_idx18 | AccessShareLock | t | f
74350 | t1_c1_idx19 | AccessShareLock | t | f
(22 rows)
注意事项:
- 为简洁起见,我们仅请求
locktype为relation的锁。通常情况下,我们可能也对其他类型的锁感兴趣。 - 为了查看关系名,我们将
oid值转换为regclass,这是获取表/索引名称的最简便方式 (例如,select 74298::oid::regclass返回t1)。 - 锁模式
AccessShareLock是最"弱"的锁,它会阻止诸如DROP TABLE、REINDEX、某些类型的ALTER TABLE/INDEX操作。通过锁定表及其所有索引,Postgres 可确保它们在我们的事务期间始终存在。 - 再次强调:所有索引都使用
AccessShareLock锁定。 - 在这种情况下,所有锁都已被授予。有人可能认为这对于
AccessShareLock来说总是如此,但并非如此 — 如果有已获取 (granted) 或挂起 (pending) 的AccessExclusiveLock(最"强"的锁),那么我们尝试获取的AccessShareLock将处于挂起状态。什么时候可能出现挂起的AccessExclusiveLock?例如,当尝试获取AccessExclusiveLock(如ALTER TABLE) 时,但有一些长时间的AccessShareLock— 形成了"夹心"情况。这种场景可能导致停机时间,例如在尝试执行一个非常简单的ALTER TABLE .. ADD COLUMN操作时,如果没有采取适当的预防措施 (如设置较低的lock_timeout并进行重试),它可能会被一个长时间运行的SELECT阻塞,然后又会阻塞后续的SELECT(以及其他DML操作)。更多内容请参考:Zero-downtime Postgres schema migrations need this: lock_timeout and retries. - 仅有 16 个锁的
fastpath为true。当fastpath=false时,Postgres 锁管理器使用一种较慢但更全面的方法来获取锁。这个问题在第 18 天的文章中有讨论:Over-indexing。
我感
文章中提到的锁冲突表格如下,也挺清晰:
| Runs concurrently with | SELECT | INSERT UPDATE DELETE | CREATE INDEX CONC VACUUM ANALYZE | CREATE INDEX | CREATE TRIGGER | ALTER TABLE DROP TABLE TRUNCATE VACUUM FULL |
|---|---|---|---|---|---|---|
| SELECT | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| INSERT UPDATE DELETE | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| CREATE INDEX CONC VACUUM ANALYZE | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| CREATE INDEX | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
| CREATE TRIGGER | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| ALTER TABLE DROP TABLE TRUNCATE VACUUM FULL | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
另外,重锁会形成一个公平的等待队列 (这一点不用于行级锁)。如果进程试图获取与当前锁或与队列中其他进程已请求的锁不兼容的锁,那么这个进程便会加入队列。