PostgreSQL中的锁:1.关系锁

之前的两篇文章集中讨论了隔离,多版本主义和日记

在本系列中,我们将讨论锁。 我会坚持这个词,但是在文学中可能还会有另一个词: 城堡

该周期将包括四个部分:

  1. 关系锁(本文);
  2. 行锁 ;
  3. 其他对象的锁和谓词锁;
  4. 锁定RAM

所有文章的内容均基于我和Pavel pluzanov所做的行政培训课程 ,但不再逐字重复,旨在用于深思熟虑的阅读和独立的实验。



有关锁的一般信息


PostgreSQL使用了许多不同的机制来阻止某些东西(或者至少被称为那)。 因此,我将从最笼统的词开始,以说明为什么根本需要锁,锁是什么以及它们之间有何不同。 然后,我们将看到在PostgreSQL中可以找到这种不同的东西,然后,我们才开始详细处理不同类型的锁。

锁用于简化对共享资源的并发访问。

竞争访问是指同时访问多个流程。 这些过程本身可以在分时模式下并行执行(如果设备允许),也可以顺序执行-这并不重要。

如果没有竞争,那么就不需要锁(例如,共享缓冲区高速缓存需要锁,而本地缓冲区不需要)。

在访问资源之前,进程必须获取与该资源关联的锁。 也就是说,我们谈论的是某种纪律:只要所有过程都符合访问共享资源的既定规则,一切都会起作用。 如果DBMS管理锁,则它本身会监视顺序; 如果应用程序设置了阻止,则此义务落在他身上。

在较低的级别上,锁由共享内存的一部分表示,在其中以某种方式指出了锁是空闲的还是已捕获的(并且可能还会记录其他信息:进程号,捕获时间等)。

您可能会注意到,这样的共享内存本身就是可以竞争访问的资源。 如果我们往下面看,我们将看到操作系统提供的特殊附件原语(例如信号量或互斥量)用于组织访问。 它们的含义是访问共享资源的代码一次只能在一个进程中执行。 在最底层,这些原语是基于原子处理器指令(例如,测试和设置或比较和交换)实现的。

在进程不再需要资源之后,它将释放锁,以便其他人可以使用该资源。

当然,锁定锁并非总是可能的:该资源可能已被其他人占用。 然后,该过程要么进入等待队列(如果锁定机制提供了此机会),要么在一定时间后重试以获取锁定。 一种或另一种方式导致以下事实:在预期资源释放的过程中,进程被迫处于空闲状态。

有时可以应用其他非阻塞策略。 例如, 多重版本控制机制在某些情况下允许多个进程同时处理不同版本的数据,而不会相互阻塞。

原则上,如果只能明确地标识该资源并与阻塞地址进行匹配,则受保护的资源可以是任何东西。

例如,资源可以是DBMS正在使用的对象,例如数据页(由文件名和文件内的位置标识),表(系统目录中的oid),表行(页和页内的偏移量)。 资源可以是内存中的结构,例如哈希表,缓冲区等(由预分配的编号标识)。 有时,使用没有任何物理意义的抽象资源甚至很方便(它们仅由唯一数字标识)。

锁的有效性受许多因素影响,我们将其中两个因素区分开。

  • 如果资源形成层次结构,则粒度 (granularity)很重要。

    例如,一个表由包含表行的页面组成。 所有这些对象都可以充当资源。 如果进程通常只对几行感兴趣,并且锁定是在表级别设置的,则其他进程将无法同时处理不同的行。 因此,粒度越高,并行化的可能性越好。

    但是,这导致锁的数量增加(必须将其存储在内存中的信息)。 在这种情况下,可以增加的级别 (升级):当低级别的粒度锁的数量超过某个限制时,它们将被更高级别的一个锁代替。
  • 可以用不同的模式捕获锁。

    模式的名称可以是绝对任意的,只有它们相互兼容的矩阵很重要。 与任何模式(包括自身)不兼容的模式通常称为互斥或互斥。 如果这些模式兼容,那么可以同时通过多个过程来捕获锁; 这种模式称为共享。 通常,可以区分出更多彼此兼容的不同模式,从而为并行性创造了更多机会。

根据使用时间,锁可以分为长型和短型。

  • 长期锁定可能会捕获很长时间(通常直到事务结束),并且通常与诸如表(关系)和行之类的资源有关。 PostgreSQL通常自动管理这些锁,但是用户仍然可以对此过程进行一些控制。

    长锁具有大量模式,因此可以对数据执行尽可能多的同时动作。 通常,对于此类锁,存在发达的基础结构(例如,支持等待队列和死锁检测)和监视工具,因为维护所有这些便利设施的成本仍比保护数据的操作成本低得多。
  • 短期锁定会在很短的时间内捕获(从几条处理器指令到几分之一秒的时间),通常是指共享内存中的数据结构。 PostgreSQL完全自动地管理这些锁-您只需要知道它们的存在即可。

    短锁的特征在于最少的模式(独占和共享)和简单的基础结构。 在某些情况下,甚至可能没有监视工具。

PostgreSQL使用不同类型的锁。

对象级别的锁是长期的“重量级” 。 这里的资源是关系和其他对象。 如果阻止一词出现在文本中而没有澄清,则表示仅是这种“正常”阻止。

在长期锁中, 行级锁分别突出。 它们的实现与其他长期锁定有所不同,因为它们的数量可能很大(可以想象在一个事务中更新一百万行)。 这种锁将在下一篇文章中讨论。

本系列的第三篇文章将专门介绍对象级别上的其余锁以及谓词锁 (因为有关所有这些锁的信息都以相同的方式存储在RAM中)。

短锁包括RAM结构的各种 。 我们将在周期的最后一篇文章中对它们进行讨论。

对象锁


因此,我们从对象级别锁开始。 在这里,对象首先被理解为关系 ,即表,索引,序列,物化表示,还有一些其他实体。 这些锁通常可以防止对象同时更改或在对象更改时被使用,也可以用于其他需求。

措辞模糊? 这是因为该组中的锁有多种用途。 使它们团结起来的是如何安排它们。

装置


对象锁位于服务器的共享内存中。 它们的数量受两个参数的值的乘积限制: max_locks_per_transaction × max_connections

锁池对于所有事务都是通用的,也就是说,一个事务可以捕获比max_locks_per_transaction更多的锁:仅重要的一点是,系统中的锁总数不超过设置的限制。 该池是在启动时创建的,因此更改指示的两个选项中的任何一个都需要重新引导服务器。

可以在pg_locks视图中查看所有锁。

如果资源已经被锁定在不兼容模式下,则尝试捕获该资源的事务将排队,并等待释放该锁定。 待处理的事务不会消耗处理器资源:释放资源时,相应的服务进程将“入睡”并由操作系统唤醒。

可能出现死锁死锁情况,其中一个事务需要第二个事务占用的资源才能继续,而第二个事务则需要第一个事务占用的资源(通常情况下,可能发生死锁和两个以上的事务)。 在这种情况下,等待将无限期地继续,因此PostgreSQL自动检测到这种情况并中止其中一个事务,以便其他事务可以继续工作。 (我们将在下一篇文章中进一步讨论死锁。)

对象类型


这是我们将在本文和下一篇文章中处理的锁的类型(或者,如果您喜欢,对象的类型)的列表。 名称是根据pg_locks视图的locktype列给出的。

  • 关系

    关系锁。
  • transactionidvirtualxid

    阻止交易编号(真实或虚拟)。 每个事务本身都拥有其自己的编号的排它锁,因此在您需要等到另一笔事务结束时,使用这些锁很方便。
  • 元组

    字符串版本锁。 在某些情况下,它用于在希望锁定同一行的多个事务之间设置优先级。

我们将把其余类型的锁的讨论推迟到本周期的第三篇文章中。 仅在特殊模式下或在独占和共享下捕获所有这些对象。

  • 延伸

    在将页面添加到任何关系的文件时使用。
  • 对象

    锁定非关系对象(数据库,架构,订阅等)。
  • 页面

    页面锁定很少使用,并且仅由某些类型的索引使用。
  • 咨询

    建议的阻止功能,由用户手动设置。

关系锁


为了不丢失上下文,我将在此类图片上标记那些类型的锁,稍后将对其进行讨论。



模式


如果不是最重要的,那么肯定是最“粗俗”的封锁-封锁关系。 对于她来说,定义了多达8种不同的模式。 此数量是必需的,以便可以同时执行属于一个表的最大数量的指令。

认真学习这些模式或试图理解其名称的含义是没有意义的。 最主要的是在正确的时间在您眼前有一个矩阵 ,它可以显示哪些锁彼此冲突。 为了方便起见,此处与需要适当锁定级别的命令示例一起复制:

锁定模式作为RS稀土小号SREËe示例SQL命令
访问共享X选择
行共享XX选择更新/分享
行排他XXXX插入,更新,删除
分享更新独家XXXXX真空,更改表* ,同时创建索引
分享到XXXXX创建索引
分享行独家XXXXXX创建触发器,更改表*
独家的XXXXXXX刷新垫。 同时查看
独家访问XXXXXXXX拖放,截断,真空已满,锁定表,更改表* ,刷新垫。 查看

一些评论:

  • 前四种模式允许同时更改表中的数据,而后四种不允许。
  • 第一种模式(访问共享)最弱,它与最后一种模式(访问专用)兼容。 最后一种模式是排他的,它与任何模式都不兼容。
  • 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); 

在第二个会话中,启动事务。 我们需要一个服务流程号。

 | => SELECT pg_backend_pid(); 
 | pg_backend_pid | ---------------- | 4746 | (1 row) 

新启动的事务持有什么锁? 我们看一下pg_locks:

 => SELECT locktype, relation::REGCLASS, virtualxid AS virtxid, transactionid AS xid, mode, granted FROM pg_locks WHERE pid = 4746; 
  locktype | relation | virtxid | xid | mode | granted ------------+----------+---------+-----+---------------+--------- virtualxid | | 5/15 | | ExclusiveLock | t (1 row) 

正如我已经说过的,事务始终持有其自身编号的排他锁(ExclusiveLock),在这种情况下为虚拟锁。 此过程没有其他锁定。

现在更新表格行。 情况将如何改变?

 | => UPDATE accounts SET amount = amount + 100 WHERE acc_no = 1; 

 => \g 
  locktype | relation | virtxid | xid | mode | granted ---------------+---------------+---------+--------+------------------+--------- relation | accounts_pkey | | | RowExclusiveLock | t relation | accounts | | | RowExclusiveLock | t virtualxid | | 5/15 | | ExclusiveLock | t transactionid | | | 529404 | ExclusiveLock | t (4 rows) 

现在,可变表和索引(为主键创建)上有锁,由UPDATE命令使用。 两种锁定均在RowExclusiveLock模式下进行。 另外,添加了对真实交易编号的排他性阻止(交易开始更改数据后立即出现)。

现在,在另一个会话中,我们将尝试在表上创建索引。

 || => 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, granted FROM pg_locks WHERE pid = 4782; 
  locktype | relation | virtxid | xid | mode | granted ------------+----------+---------+-----+---------------+--------- virtualxid | | 6/15 | | ExclusiveLock | t relation | accounts | | | ShareLock | f (2 rows) 

我们看到事务正在尝试以ShareLock模式获得表锁,但不能(已授予= f)。

使用9.6版中出现的函数可以很方便地找到阻塞进程的编号,通常可以找到几个编号(在此之前,我必须仔细查看pg_locks的所有内容来得出结论):

 => SELECT pg_blocking_pids(4782); 
  pg_blocking_pids ------------------ {4746} (1 row) 

然后,要了解情况,您可以获取有关会话的信息,其中包括找到的数字:

 => SELECT * FROM pg_stat_activity WHERE pid = ANY(pg_blocking_pids(4782)) \gx 
 -[ RECORD 1 ]----+------------------------------------------------------------ datid | 16386 datname | test pid | 4746 usesysid | 16384 usename | student application_name | psql client_addr | client_hostname | client_port | -1 backend_start | 2019-08-07 15:02:53.811842+03 xact_start | 2019-08-07 15:02:54.090672+03 query_start | 2019-08-07 15:02:54.10621+03 state_change | 2019-08-07 15:02:54.106965+03 wait_event_type | Client wait_event | ClientRead state | idle in transaction backend_xid | 529404 backend_xmin | query | UPDATE accounts SET amount = amount + 100 WHERE acc_no = 1; backend_type | client backend 

事务完成后,将释放锁并创建索引。

 | => COMMIT; 
 | COMMIT 

 || CREATE INDEX 

在排队!


为了更好地想象不兼容锁的外观会导致什么,我们将看到如果在系统运行期间执行VACUUM FULL命令会发生什么。

让SELECT命令首先在我们的表上执行。 她锁定了最弱的访问共享级别。 为了控制锁释放时间,我们在事务内部执行此命令-直到事务结束,才会释放锁。 实际上,几个命令可以读取(和修改)表,而某些查询可能要花相当长的时间。

 => 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_for FROM 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; --  VACUUM FULL 

 => SELECT locktype, mode, granted, pid, pg_blocking_pids(pid) AS wait_for FROM 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_for FROM 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_for FROM 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)才能够捕获相应的锁(访问共享)并执行。

 | => 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参数值为一秒:

 => 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; 

第二个UPDATE命令要求锁定。 请稍等,然后完成第一笔交易。

 => SELECT pg_sleep(1); => COMMIT; 
 COMMIT 

现在,第二笔交易即可完成。

 | UPDATE 1 
 | => COMMIT; 
 | COMMIT 

所有重要信息都进入了期刊:

 postgres$ tail -n 7 /var/log/postgresql/postgresql-11-main.log 
 2019-08-07 15:26:30.827 MSK [5898] student@test LOG: process 5898 still waiting for ShareLock on transaction 529427 after 1000.186 ms 2019-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 ms 2019-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; 

待续

Source: https://habr.com/ru/post/zh-CN462877/


All Articles