PostgreSQL 锁 之 关系级锁
阅读原文时间:2022年04月22日阅读:1

1.关于锁的基本信息

PostgreSQL 有各种各样的技术来锁定某些东西(或者至少是这样称呼的)。因此,我将首先用最笼统的术语解释为什么需要锁,可用的锁类型以及它们之间的区别。然后我们将弄清楚 PostgreSQL 中使用了哪些种类的锁,然后我们将开始详细讨论不同种类的锁。

锁用于对共享资源的并发访问进行排序。

并发访问是指多个进程的同时访问。这些进程本身既可以并行运行(如果硬件允许),也可以以分时模式顺序运行,没有区别。

如果没有并发,就不需要锁(例如:共享缓冲区缓存需要锁,而本地缓存则不需要)。

在访问资源之前,进程必须获取与该资源关联的锁。所以这是一个一定的规则问题:一切正常,而进程符合访问共享资源的既定规则。如果 DBMS 控制锁,它会自行维护顺序;但是如果一个应用程序设置了锁,责任就落在了它身上。

在低层次上,锁是共享内存中的一个区域,带有一些指示是否释放或获取锁(可能还有一些附加信息:进程号、获取时间等)。

请注意,共享内存中的这个区域本身是一个允许并发访问的资源。如果我们下降到较低的级别,我们将看到为了规范访问,操作系统提供了专门的同步原语(例如信号量或互斥体)。它们的目的是确保访问共享资源的代码仅在一个进程中使用。在最低级别,这些原语是通过原子处理器指令(例如test-and-set或compare-and-swap)实现的。

当某个进程不再需要某个资源时,后者会释放锁,以便其他进程可以使用它。

当然,有时无法获得锁:资源可能已被其他人使用。然后该进程要么处于等待队列中(如果锁技术允许这样做),要么在一段时间后重复尝试获取锁。无论如何,该进程必须在等待资源空闲时处于空闲状态。

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

一般来说,要保护的资源是指我们可以明确识别并将锁地址与其相关联的任何东西。

例如:一个 DBMS 对象,如数据页(由文件中的文件名和位置标识)、表(系统目录中的 OID)或表行(其中的页和偏移量)可以是资源。诸如哈希表、缓冲区等(由先前分配的编号标识)之类的内存结构也可以是资源。有时使用没有物理意义的抽象资源(仅由唯一编号标识)甚至更方便。

影响加锁效率的因素很多,我们重点介绍两个。

  • 当资源按层次结构组织时,粒度至关重要。

例如:一个表由包含表行的页组成。所有这些对象都可以是资源。如果进程只对几行感兴趣,但在表级别获得了锁,则其他进程将无法同时处理不同的行。因此,粒度越高,越有利于并行化。

但这会导致锁的数量增加(信息需要存储在内存中)。在这种情况下,可以应用锁的升级:当低级高粒度锁的数量超过一定限制时,将它们替换为一个更高级别的锁。

  • 可以通过多种方式获取锁。

模式的名称可以是任何名称;真正重要的是它们相互兼容的矩阵。与任何模式(包括自身)不兼容的模式通常称为独占。如果模式兼容,多个进程可以同时获取锁;像这样的模式称为shared。一般来说,可以区分的相互兼容的模式越多,并发的机会就越多。

根据持续时间,锁可以分为长锁和短锁。

  • 长锁的获取时间可能很长(通常直到事务结束),并且通常与表(关系)和行等资源相关。作为一项规则,PostgreSQL 会自动控制这些锁,但是用户对这个过程有一定的控制权。

大量模式是长锁的典型特征,以启用尽可能多的同时数据操作。通常有一个广泛的基础设施(例如:支持等待队列和检测死锁)和监控工具可用于此类锁,因为所有这些便利功能的维护成本无论如何都比受保护数据的操作成本低得多。

  • 短锁的获取时间很短(从几条处理器指令到几分之一秒),通常与共享内存中的数据结构有关。PostgreSQL 以全自动方式控制这些锁——你只需要知道它们的存在。

最少的模式(独占和共享)和简单的基础设施是短锁的典型特征。有时甚至可能没有监控工具。

PostgreSQL 使用不同类型的锁。

对象级锁属于长的、“重量级”的锁。关系和其他对象是这里的资源。如果您在本文中遇到“锁”或“锁定”这个词而没有明确说明,那么它的意思就是这种“普通”锁定。

在长锁中,行级锁是比较突出的。它们的实现方式与其他长锁不同,因为它们的数量可能很大(想象一下在一个事务中更新一百万行)。我们将在下一篇文章中讨论这些锁。

对象级锁

我们从对象级锁开始。这里的对象主要是指关系,即表、索引、序列和物化视图,但也包括其他一些实体。这些锁通常用于保护对象免受同时更改或在更改对象时使用,但也用于其他需要。

措辞很模糊,不是吗?正是如此,因为该组中的锁用于各种目的。唯一将它们结合在一起的是它们的组织方式。

组织

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

锁池是所有事务的一个,也就是说,一个事务可以获得比max_locks_per_transaction更多的锁:唯一重要的是系统中的锁总数不超过指定的数量。池是在启动时创建的,因此要更改上述两个参数中的任何一个,都需要重新启动服务器。

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

如果资源已经以不兼容的模式锁定,则尝试获取锁的事务将排队等待直到锁被释放。等待事务不消耗处理器资源:后端进程涉及“入睡”,并在资源空闲时由操作系统唤醒。

当要继续工作时,一个事务需要另一个事务正在使用的资源,而第二个事务需要第一个事务正在使用的资源,就会发生死锁。通常,可能会出现两个以上事务的死锁。在这种情况下,等待将无限持续,因此,PostgreSQL 会自动检测到这种情况并中止其中一个事务以使其他事务继续。(我们将在下一篇文章中详细讨论死锁。)

对象类型

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

  • 关系

锁定关系。

  • transactionid和virtualxid

锁定事务 ID(实际或虚拟)。每个事务本身在自己的 ID 上都持有一个排他锁,因此,当我们需要等待另一个事务完成时,使用这种锁很方便。

  • 元组

锁定一个元组。在某些情况下用于优先考虑等待同一行上的锁定的多个事务。

  • 扩展

在将页面添加到某种关系的文件时使用。

  • 目的

锁定不同于关系的对象(数据库、模式、订阅等)。

页面上的锁定 — 不经常使用且仅由某些类型的索引使用。

  • 咨询

咨询锁——用户手动获取它们。

2.关系级锁

模式

除非关系级锁是锁中最重要的,否则它的模式肯定最多。为它定义了多达 8 种不同的模式。需要这么多才能同时执行与一张表相关的最大可能数量的命令。

将这些模式记入您的记忆或试图了解它们的名称是没有意义的。真正重要的是手边有显示哪些锁相互冲突的矩阵。为方便起见,此处提供了它以及需要相应级别锁定的命令示例:

  • 前 4 种模式允许并发更改表数据,而后 4 种不允许。

  • 第一种模式(Access Share)是最弱的,它与除最后一种(Access Exclusive)之外的任何其他模式都兼容。最后一种模式是独占的,它与任何其他模式都不兼容。

  • ALTER TABLE 命令有多种风格,不同的风格需要不同级别的锁。因此,此命令出现在矩阵的不同行中,并用星号标记。

演示

让我们考虑一个例子。如果我们执行 CREATE INDEX 命令会发生什么?

我们从文档中了解到,此命令获取共享锁。从矩阵中,我们了解到该命令与自身兼容(即可以同时创建多个索引)以及与读取命令兼容。因此,SELECT 命令将继续工作,而 UPDATE、DELETE 和 INSERT 将被阻止。

反之亦然:更改表数据的未完成事务将阻止 CREATE INDEX 命令的执行。正是由于这个原因,该命令的 CREATE INDEX CONCURRENTLY 风格才可用。它的执行需要更长的时间(甚至可能因错误而失败),但它允许并发数据更新。

您可以在实践中确保这一点。对于实验,我们将使用“银行”账户表,这是我们从第一个系列开始就熟悉的,其中将存储帐号和金额。

  • => CREATE TABLE pgccc_accounts(

  • acc_no integer PRIMARY KEY,

  • amount numeric

  • );

  • => INSERT INTO pgccc.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, granted

  • FROM pg_locks

  • WHERE pid = 4746;

  • locktype  | relation | virtxid | xid |     mode      | granted

  • ------------+----------+---------+-----+---------------+---------

  • virtualxid |          | 5/15    |     | ExclusiveLock | t

  • (1 row)

正如我之前所说,一个事务总是在它自己的 ID 上持有一个独占锁,在这种情况下它是虚拟的。此进程没有其他锁。

现在让我们更新一个表格行。情况将如何变化?

正在更改的表和 UPDATE 命令使用的索引(为主键创建)上的锁出现了。获得的两个锁都是行独占的。此外,还添加了对实际事务 ID 的排他锁(该 ID 在事务开始更改数据时出现)。

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

该命令“hangs”等待资源空闲。它特别尝试获取什么锁?让我们弄清楚:

现在很清楚,事务试图获取表上的共享锁,但不能(granted = f)。

为了找到加锁进程的进程ID(pid),一般是几个pid,使用9.6版本出现的功能比较方便(之前必须仔细查看所有内容才能得出结论) ) pg_locks:

然后,为了了解情况,我们可以获取有关找到的 pid 所属会话的信息:

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

3.加入队列

为了更好地理解不兼容锁的发生意味着什么,让我们看看如果我们在系统运行期间执行 VACUUM FULL 命令会发生什么。

让 SELECT 成为在上表中执行的第一个命令。它获得了最弱级别的访问共享锁。为了控制释放锁的时间,我们在事务内部执行这个命令——直到事务完成,锁才会被释放。实际上,几个命令可以读取(和更新)一个表,并且执行一些查询可能需要很长时间。

  • => BEGIN;

  • => SELECT * FROM pgccc_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 = 'pgccc_accounts'::regclass;

然后管理员执行 VACUUM FULL 命令,该命令需要一个具有 Access Exclusive 级别的锁,并且与所有内容都不一致,即使是 Access Share。(LOCK TABLE 命令需要相同的锁。)并且事务已排队。

但是应用程序继续发出查询,因此系统中也会出现 SELECT 命令。假设它可以在 VACUUM FULL 等待时“通过”,但不是——它确实在等待 VACUUM FULL。

当使用 SELECT 命令的第一个事务完成并释放锁时,VACUUM FULL 命令(我们通过 LOCK TABLE 命令模拟)启动。

  • => COMMIT;

  • COMMIT

  • LOCK TABLE

并且只有在 VACUUM FULL 完成并释放锁之后,所有排队的命令(本例中为 SELECT)才能获得适当的锁(访问共享)并执行。

  • => COMMIT;

  • COMMIT

  • acc_no | amount

  • --------+---------

  • 2 | 2000.00

  • 3 | 3000.00

  • 1 | 1100.00

  • (3 rows)

因此,不正确执行的命令可能会在比执行命令本身更长的时间间隔内使系统工作瘫痪。

4.监控工具
毫无疑问,正确的工作需要锁,但它们会导致不必要的等待。可以跟踪这些等待以找出其根本原因并尽可能消除它(例如:通过更改应用程序的算法)。

我们已经熟悉了一种方法:当发生长锁时,我们可以查询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 pgccc_accounts

  • SET amount = amount - 100.00

  • WHERE acc_no = 1;

  • UPDATE 1

  • |  => BEGIN;

  • |  => UPDATE pgccc_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

  • 2022-03-31 15:26:30.827 MSK [5898] student@test LOG:  process

  • 5898 still waiting for ShareLock on transaction 529427 after

  • 1000.186 ms

  • 2022-03-31 15:26:30.827 MSK [5898] student@test DETAIL:

  • Process holding the lock: 5862. Wait queue: 5898.

  • 2022-03-31 15:26:30.827 MSK [5898] student@test CONTEXT:

  • while updating tuple (0,4) in relation "pgccc_accounts"

  • 2022-03-31 15:26:30.827 MSK [5898] student@test STATEMENT:

  • UPDATE accounts SET amount = amount + 100.00

  • WHERE acc_no = 1;

  • 2022-03-31 15:26:30.836 MSK [5898] student@test LOG:

  • process 5898 acquired ShareLock on transaction 529427

  • after 1009.536 ms

  • 2022-03-31 15:26:30.836 MSK [5898] student@test CONTEXT:

  • while updating tuple (0,4) in relation "pgccc_accounts"

  • 2022-03-31 15:26:30.836 MSK [5898] student@test STATEMENT:  UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;

原地址链接:

https://postgrespro.com/blog/pgsql/5967999

PG考试咨询