锁的基本信息
PostgreSQL有各种各样的技术来锁定某些东西(或者至少被称为so)。因此,我将首先以最笼统的方式解释为什么需要锁,锁的类型以及它们之间的区别。然后,我们将弄清楚PostgreSQL中使用了哪种类型的锁,只有在此之后,我们才开始详细讨论不同类型的锁。
锁定用于命令对共享资源的并发访问。
并发访问是指同时访问多个进程。这些进程本身既可以并行运行(如果硬件允许),也可以按分时模式顺序运行-没什么区别。
没有并发,就不需要锁定(例如:共享缓冲区高速缓存需要锁定,而本地缓存则不需要)。
在访问资源之前,进程必须获取与此资源关联的锁。因此,这是一个特定的原则问题:当进程遵循访问共享资源的既定规则时,一切都会正常工作。如果DBMS控制锁定,它将自行维护顺序;但如果应用程序设置锁,则责任就落在它身上。
在较低的级别上,锁是共享内存中的一个区域,它指示锁是被释放还是被获取(可能还有一些附加信息):进程号、获取时间等等。
请注意,共享内存中的这个区域本身就是一个允许并发访问的资源。如果我们下降到一个较低的层次,我们将看到,为了规范访问,操作系统提供了专门的同步primitives (如信号量或互斥)。它们的目的是确保访问共享资源的代码只在一个进程中使用。在最底层,这些primitives 是通过原子处理器指令(如test-and-set
或compare-and-swap
)实现的。
当某个进程不再需要资源时,后者会释放该锁,以便其他进程可以使用它。
当然,有时无法获取锁:资源可能已被其他人使用。然后,进程要么站在等待队列中(如果锁定技术允许这样做),要么在一段时间后重复尝试获取锁定。无论如何,该进程必须处于空闲状态以等待资源释放。
有时,可以使用其他非阻塞性策略。例如:多版本并发控制在某些情况下,允许多个进程同时处理不同版本的数据,而不会相互阻塞。
通常,对于要保护的资源,我们指的是我们可以明确标识并将锁地址与其关联的任何内容。
例如:DBMS对象,例如数据页(由文件名和文件内的位置标识)、表(系统目录中的OID
)或表行(其中的page
和offset
)可以是资源。内存结构,如哈希表、缓冲区等(由先前分配的编号标识)也可以是资源。有时,使用没有物理意义(仅由唯一数字标识)的抽象资源甚至更为方便。
影响锁存效率的因素很多,其中有两个是我们重点考虑的。
当资源按层次结构组织时,粒度是至关重要的。
例如:表由
pages
组成,pages
中包含表行。所有这些对象都可以是资源。如果进程只对几行感兴趣,但在表级别获得了锁,其他进程则将无法同时处理不同的行。因此,粒度越高,就越有利于实现并行化。但这会导致锁的数量增加(需要将信息存储在内存中)。在这种情况下,可以应用锁升级:当低级别高粒度锁的数量超过某个限制时,它们将被一个更高级别的锁替换。
可以以各种模式获取锁。
模式的名称可以是任意的;真正重要的是它们之间的兼容性矩阵。与任何模式(包括其自身)不兼容的模式通常称为独占模式。如果模式是兼容的,几个进程可以同时获得一个锁;这样的模式称为共享模式。一般来说,可以区分的模式越多,并发的机会就越多。
按持续时间,锁可以分为长锁和短锁。
长时锁可能需要很长一段时间(通常直到事务结束),并且通常与表(关系)和行等资源相关。通常,PostgreSQL自动控制这些锁,但是用户对这个过程有一定的控制权。
长锁通常具有大量模式,以实现尽可能多的同时数据操作。通常,此类锁定可使用广泛的基础结构(例如:支持等待队列和检测死锁)和监视工具,因为所有这些便利功能的维护成本无论如何都比受保护数据的操作成本低得多。
短时锁定是在短时间内(从几条处理器指令到几分之一秒)获得的,通常与共享内存中的数据结构有关。PostgreSQL以全自动的方式控制此类锁-您只需要知道它们的存在即可。
最小模式(独占和共享)和简单的基础结构是短锁的典型特征。有时甚至没有监视工具。
PostgreSQL使用不同类型的锁。
对象级锁属于长的«heavy weight»锁定。关系和其他对象在这里都是资源。
在长锁中,行级锁单独突出。它们的实现方式与其他长锁不同,因为它们的数量可能很大(想象一下,在一个事务中更新一百万行)。我们将在下一篇文章中讨论这些锁。
本系列的第三篇文章将介绍其余的对象级锁以及谓词锁(因为所有这些锁的信息都统一存储在RAM中)。
短锁包括RAM结构上的各种锁。我们将在本系列的最后一篇文章中讨论它们。
对象级锁
我们从对象级锁开始。这里的对象主要是关系,即表,索引,序列和实例化视图,还有其他一些实体。这些锁通常用于防止对象同时更改或在更改对象时使用,也可用于其他需要。
组织
对象锁存储在服务器的共享内存中。它们的数量受两个参数值的乘积限制:max_locks_per_transaction×max_connections
。
锁池是所有事务的池,也就是说,一个事务可以比max_locks_per_transaction
获得更多的锁:唯一重要的是,系统中的锁总数不超过指定的数量。该池是在启动时创建的,因此要更改以上两个参数中的任何一个,都需要重新启动服务器 .
您可以在pg_locks
视图中看到所有锁。
如果资源已经以不兼容模式锁定,则尝试获取该锁的事务将排队等待,直到释放该锁为止。等待中的事务不会消耗处理器资源:后端进程涉及“进入睡眠状态”,并在资源释放时由操作系统唤醒。
当继续工作时,一个事务需要另一个事务正在使用的资源,而第二个事务需要第一个事务正在使用的资源,就会出现死锁。通常,会出现两个以上事务的死锁。在这种情况下,等待将无限持续,因此,PostgreSQL会自动检测到这种情况,并中止其中一个事务以使其他事务继续。(我们将在下一篇文章中详细讨论死锁。)
当继续工作时,一个事务需要另一个事务使用的资源,而第二个事务需要第一个事务使用的资源,则会发生死锁。通常,可能会发生两个以上事务的死锁。在这种情况下,等待将无限期地持续,因此,PostgreSQL自动检测到这种情况,并中止其中一个事务以使其他事务继续进行。(我们将在下一篇文章中详细讨论死锁。)
对象类型
以下是我们将在本篇和下一篇文章中处理的锁类型(或者叫对象类型)的列表。名称是根据pg_locks
视图的locktype
列提供的。
relation
锁定关系。
transactionid и virtualxid
锁定事务ID(实际或虚拟)。每个事务本身都对自己的ID持有排他锁,因此,当我们需要等到另一个事务完成,使用这些锁很方便。
tuple
锁定元组。在某些情况下用于优先处理等待同一行锁定的多个事务。
在本系列的第三篇文章之前,我们将讨论其他类型的锁。它们都是以独占方式或共享/独占方式获取的。
extend
将pages添加一些关系的文件时使用。
object
锁定与关系(数据库,架构,预订等)不同的对象。
page
page上的锁-很少使用,并且仅由某些类型的索引使用。
advisory
咨询锁-用户手动获取它们。
关系级锁
为了不脱离上下文,在这样的图中,我将标记这些类型的锁,这些锁将在后面进一步讨论。
模式
关系级锁是锁中最重要的锁,提供了丰富的模式。定义了多达8种不同的模式。要同时执行与一个表有关的最大数量的命令,需要很多 。
将这些模式提交给您的内存或试图深入了解它们的名称是没有意义的。真正重要的是手头有一个矩阵,显示哪些锁相互冲突。为了方便起见,此处提供了它以及需要相应锁定级别的命令示例:
Locking mode | AS | RS | RE | SUE | S | SRE | E | AE | Example of SQL commands |
---|---|---|---|---|---|---|---|---|---|
Access Share | X | SELECT | |||||||
Row Share | X | X | SELECT FOR UPDATE/SHARE | ||||||
Row Exclusive | X | X | X | X | INSERT, UPDATE, DELETE | ||||
Share Update Exclusive | X | X | X | X | X | VACUUM, ALTER TABLE*, СREATE INDEX CONCURRENTLY | |||
Share | X | X | X | X | X | CREATE INDEX | |||
Share Row Exclusive | X | X | X | X | X | X | CREATE TRIGGER, ALTER TABLE* | ||
Exclusive | X | X | X | X | X | X | X | REFRESH MAT. VIEW CONCURRENTLY | |
Access Exclusive | X | X | X | X | X | X | X | X | DROP, TRUNCATE, VACUUM FULL, LOCK TABLE, ALTER TABLE*, REFRESH MAT. VIEW |
一些评论:
前四个模式允许并发更改表数据,而后四个模式则不允许。
第一种模式(访问共享)是最弱的,它与除最后一种(访问独占)之外的任何其他模式兼容。最后一种模式是独占的,它与任何其他模式都不兼容。
ALTER TABLE命令具有多种形式,并且不同的形式需要不同级别的锁。因此,此命令出现在矩阵的不同行中,并标有星号。
举例说明
让我们考虑一个例子。如果执行CREATE INDEX命令会怎样?
我们从上表中了解到,这个命令获得了一个共享锁。从矩阵中,我们了解到该命令与自身(即可以同时创建多个索引)和读取命令兼容。所以,SELECT命令将继续工作,而UPDATE、DELETE和INSERT将被阻止。
反之亦然:更改表数据的未完成事务将阻止CREATE INDEX命令的执行。正是因为这个原因,CREATE INDEX CONCURRENTLY才可用。它的执行需要更长的时间(甚至可能因错误而失败),但它允许并发的数据更新。
你可以在实践中确保这一点。我们将使用«银行»账户表,它将存储账号和金额。
=> CREATE TABLE accounts(acc_no integer PRIMARY KEY,amount numeric);=> INSERT INTO accounts VALUES (1,1000.00), (2,2000.00), (3,3000.00);
我们将开始交易。我们将需要后端进程的进程ID。
| => SELECT pg_backend_pid();| pg_backend_pid| ----------------| 4746| (1 row)
刚开始的事务持有什么锁?调查pg_locks
:
=> SELECT locktype, relation::REGCLASS, virtualxid AS virtxid, transactionid AS xid, mode, grantedFROM pg_locks WHERE pid = 4746;locktype | relation | virtxid | xid | mode | granted------------+----------+---------+-----+---------------+---------virtualxid | | 5/15 | | ExclusiveLock | t(1 row)
就像我之前说过的,事务在其自己的ID上始终拥有排他锁,在本例中这是虚拟的。此过程没有其他锁。
现在让我们更新一行。情况将如何改变?
| => UPDATE accounts SET amount = amount + 100 WHERE acc_no = 1;=> \g locktype | relation | virtxid | xid | mode | granted---------------+---------------+---------+--------+------------------+---------relation | accounts_pkey | | | RowExclusiveLock | trelation | accounts | | | RowExclusiveLock | tvirtualxid | | 5/15 | | ExclusiveLock | ttransactionid | | | 529404 | ExclusiveLock | t(4 rows)
在被更改的表和UPDATE命令使用的索引(为主键创建)上出现锁。获取的两个锁都是行独占的。此外,还添加了对实际事务ID的排他锁(一旦事务开始更改数据,ID就会出现)。
现在,我们将在另一个会话中尝试在表上创建索引。
|| => SELECT pg_backend_pid();
|| pg_backend_pid|| ----------------|| 4782|| (1 row)|| => CREATE INDEX ON accounts(acc_no);
命令“挂起”等待资源释放。它试图获取什么锁?让我们弄清楚这一点:
=> SELECT locktype, relation::REGCLASS, virtualxid AS virtxid, transactionid AS xid, mode, grantedFROM pg_locks WHERE pid = 4782;locktype | relation | virtxid | xid | mode | granted------------+----------+---------+-----+---------------+---------virtualxid | | 6/15 | | ExclusiveLock | trelation | accounts | | | ShareLock | f(2 rows)
现在很清楚,该事务尝试获取表上的Share锁,但不能(granted = f
)。
为了找到锁定进程的进程ID(pid),以及通常的几个pid,使用pg_blocking_pids
函数是很方便的(在此之前,必须通过仔细检查pg_locks
的所有内容得出结论):
=> SELECT pg_blocking_pids(4782);pg_blocking_pids------------------{4746}(1 row)
然后,为了了解情况,我们可以获得有关pids所涉及的会话的信息:
=> SELECT * FROM pg_stat_activityWHERE pid = ANY(pg_blocking_pids(4782)) \gx-[ RECORD 1 ]----+------------------------------------------------------------datid | 16386datname | testpid | 4746usesysid | 16384usename | studentapplication_name | psqlclient_addr |client_hostname |client_port | -1backend_start | 2019-08-07 15:02:53.811842+03xact_start | 2019-08-07 15:02:54.090672+03query_start | 2019-08-07 15:02:54.10621+03state_change | 2019-08-07 15:02:54.106965+03wait_event_type | Clientwait_event | ClientReadstate | idle in transactionbackend_xid | 529404backend_xmin |query | UPDATE accounts SET amount = amount + 100 WHERE acc_no = 1;backend_type | client backend
事务完成后,将释放锁并创建索引。
| => COMMIT;
| COMMIT
|| CREATE INDEX
队列
为了更好地理解发生不兼容锁定的原因,让我们看看如果在系统运行期间执行VACUUM FULL命令会发生什么。
让SELECT成为对上表执行的第一个命令。它获得了一个最弱的级别,访问Access Share
锁。为了控制释放锁的时间,我们在事务内部执行这个命令-在事务完成之前不会释放锁。实际上,有些命令可能会读取(和更新)一个表相当长的时间。
=> BEGIN;=> SELECT * FROM accounts;
acc_no | amount--------+--------- 2 | 2000.00 3 | 3000.00 1 | 1100.00(3 rows)=> SELECT locktype, mode, granted, pid, pg_blocking_pids(pid) AS wait_forFROM pg_locks WHERE relation = 'accounts'::regclass;locktype | mode | granted | pid | wait_for----------+-----------------+---------+------+----------relation | AccessShareLock | t | 4710 | {}(1 row)
然后,管理员执行VACUUM FULL命令,该命令需要一个具有Access Exclusive
级别且与所有东西(甚至与Access Share
)不一致的锁。(LOCK TABLE命令需要相同的锁。)并且事务在排队。
| => BEGIN;| => LOCK TABLE accounts; -- the same lock mode as for VACUUM FULL
=> SELECT locktype, mode, granted, pid, pg_blocking_pids(pid) AS wait_forFROM pg_locks WHERE relation = 'accounts'::regclass; locktype | mode | granted | pid | wait_for----------+---------------------+---------+------+---------- relation | AccessShareLock | t | 4710 | {} relation | AccessExclusiveLock | f | 4746 | {4710}(2 rows)
但是应用程序继续发出查询,因此SELECT命令也出现在系统中。假设,在VACUUM FULL等待期间,它可以“通过”(坦白说不会),它排在VACUUM FULL的队列中。
|| => SELECT * FROM accounts;
=> SELECT locktype, mode, granted, pid, pg_blocking_pids(pid) AS wait_forFROM pg_locks WHERE relation = 'accounts'::regclass; locktype | mode | granted | pid | wait_for----------+---------------------+---------+------+---------- relation | AccessShareLock | t | 4710 | {} relation | AccessExclusiveLock | f | 4746 | {4710} relation | AccessShareLock | f | 4782 | {4746}(3 rows)
当使用SELECT命令的第一个事务完成并释放锁时,VACUUM FULL命令(我们通过LOCK TABLE命令模拟)启动。
=> COMMIT;
COMMIT
| LOCK TABLE
=> SELECT locktype, mode, granted, pid, pg_blocking_pids(pid) AS wait_forFROM pg_locks WHERE relation = 'accounts'::regclass; locktype | mode | granted | pid | wait_for----------+---------------------+---------+------+---------- relation | AccessExclusiveLock | t | 4746 | {} relation | AccessShareLock | f | 4782 | {4746}(2 rows)
只有在VACUUM FULL完成并释放锁之后,所有排队的命令(本例中的SELECT)才能获得适当的锁(Access Share)并执行。
| => COMMIT;
| COMMIT
|| acc_no | amount || --------+---------|| 2 | 2000.00|| 3 | 3000.00|| 1 | 1100.00|| (3 rows)
因此,一个不正确执行的命令会使系统的工作瘫痪,时间间隔比执行命令本身所需的时间间隔长得多。
监控工具
毫无疑问,正确的工作需要锁,但它们会导致不必要的等待。可以跟踪这些等待,以便找出其根本原因并尽可能消除它(例如:通过更改应用程序的算法)。
我们已经熟悉一种执行此操作的方法:长锁发生时,我们可以查询pg_locks
视图,查看锁定和锁定的事务(使用pg_blocking_pids
函数),并使用解释数据pg_stat_activity
。
另一种方法是打开log_lock_waits
参数。在这种情况下,如果事务等待的时间超过deadlock_timeout
时间(尽管参数用于死锁,但此处指的是正常等待),则信息将进入服务器消息日志。
我们试试吧。
=> ALTER SYSTEM SET log_lock_waits = on;=> SELECT pg_reload_conf();
deadlock_timeout
参数 的默认值为1秒钟:
=> SHOW deadlock_timeout; deadlock_timeout------------------ 1s(1 row)
让我们再现一个锁。
=> BEGIN;=> UPDATE accounts SET amount = amount - 100.00 WHERE acc_no = 1;
UPDATE 1
| => BEGIN;| => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
第二个更新命令正在等待锁定。让我们等待第二个,然后完成第一个事务。
=> SELECT pg_sleep(1);=> COMMIT;COMMIT
现在可以完成第二个事务。
| UPDATE 1| => COMMIT;| COMMIT
所有重要的信息都被记录下来了:
postgres$ tail -n 7 /var/log/postgresql/postgresql-11-main.log2019-08-07 15:26:30.827 MSK [5898] student@test LOG: process 5898 still waiting for ShareLock on transaction 529427 after 1000.186 ms2019-08-07 15:26:30.827 MSK [5898] student@test DETAIL: Process holding the lock: 5862. Wait queue: 5898.2019-08-07 15:26:30.827 MSK [5898] student@test CONTEXT: while updating tuple (0,4) in relation "accounts"2019-08-07 15:26:30.827 MSK [5898] student@test STATEMENT: UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;2019-08-07 15:26:30.836 MSK [5898] student@test LOG: process 5898 acquired ShareLock on transaction 529427 after 1009.536 ms2019-08-07 15:26:30.836 MSK [5898] student@test CONTEXT: while updating tuple (0,4) in relation "accounts"2019-08-07 15:26:30.836 MSK [5898] student@test STATEMENT: UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
本文翻译自:https://habr.com/en/company/postgrespro/blog/500714/