PostgreSQL 锁 之 关系级锁

本文详细探讨了PostgreSQL中的锁类型、用途、模式、粒度及并发控制,包括对象级锁、关系级锁的示例和死锁处理。通过实例解析了长锁(如表锁)与短锁(如共享内存锁)的区别,以及如何通过监控工具优化锁管理。

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考试相关详情:http://www.pgccc.com.cn/

<think>我们正在讨论PostgreSQL 8的机制以及争用的解决方案。根据引用[1],我们知道在PostgreSQL 8.1之前,共享缓冲区管理器的所有操作都由一个全局(BufMgrLock)保护,这导致了竞争问题。8.1版本之后引入了新的方案,避免了在常见代码路径上使用全局。 用户的问题是关于PostgreSQL 8的机制和争用解决方案。请注意,PostgreSQL 8是一个较老的版本(最新版本是15),但为了回答用户的问题,我们将聚焦于PostgreSQL 8(特别是8.1及之后)的机制。 ### PostgreSQL 8 机制概述 PostgreSQL 8 提供了多粒度机制,包括表、行、页等。主要分为两种模式: 1. **共享(Shared Locks)**:多个事务可以同时持有共享,用于读操作。 2. **排他(Exclusive Locks)**:一次只能由一个事务持有,用于写操作。 #### 表的模式包括: - `ACCESS SHARE`:最低别的,与大多数`ACCESS EXCLUSIVE`操作冲突。 - `ROW SHARE`:允许并发读,但禁止并发写。 - `ROW EXCLUSIVE`:写操作自动获取,与`SHARE`、`SHARE ROW EXCLUSIVE`、`EXCLUSIVE`和`ACCESS EXCLUSIVE`冲突。 - `SHARE UPDATE EXCLUSIVE`:用于`VACUUM`(不带FULL)等操作。 - `SHARE`:用于创建索引,允许读但禁止写。 - `SHARE ROW EXCLUSIVE`:禁止并发数据修改和大多数其他模式。 - `EXCLUSIVE`:允许并发读,但禁止写。 - `ACCESS EXCLUSIVE`:最高别,与所有其他模式冲突(如`DROP TABLE`、`TRUNCATE`等)。 #### 行在具体的数据行上加,包括: - `FOR UPDATE`:行排他,用于更新或删除行。 - `FOR SHARE`:行共享,允许其他事务读但禁止修改。 ### 争用问题及解决方案 #### 1. 监控争用 使用以下查询监控当前状态: ```sql SELECT pg_stat_activity.pid, pg_stat_activity.query, pg_locks.mode, pg_locks.granted FROM pg_stat_activity JOIN pg_locks ON pg_stat_activity.pid = pg_locks.pid WHERE pg_stat_activity.datname = current_database(); ``` #### 2. 解决争用 - **识别阻塞进程**:查找被阻塞的查询和阻塞它们的进程。 - **终止阻塞进程**:如果必要,使用`pg_terminate_backend(pid)`终止阻塞进程。 - **优化查询**:长事务或低效查询容易导致争用。优化查询以减少持有时间。 - **使用超时**:设置`lock_timeout`参数,使查询在等待超过指定时间后自动取消。 - **调整事务隔离别**:使用较低的隔离别(如`READ COMMITTED`)可以减少争用。 #### 3. 缓冲区优化(引用[1]) 在8.1之前,缓冲区管理器使用全局(BufMgrLock)导致竞争。8.1引入细粒度: - 每个缓冲区页使用独立的(`buffer pin`),减少竞争。 - 使用`buffer descriptors`的轻量(`LWLock`)管理缓冲区。 ### 乐观并发控制(OCC)与机制(引用[2]) 乐观并发控制(OCC)假设事务冲突很少,在提交时检查冲突。与机制(悲观)的区别: - **机制**:在操作数据前获取,防止冲突。 - **OCC**:事务执行时不加,提交时验证数据是否被修改。 **OCC适用场景**: - 低冲突环境(如读多写少)。 - 长事务(避免长时间持有)。 - 高并发读操作。 在PostgreSQL中,可以使用`SERIALIZABLE`隔离别实现类似OCC的效果(使用谓词检测冲突)。 ### 示例:避免争用的最佳实践 1. **缩短事务时间**:尽快提交事务,减少持有时间。 2. **批量操作分解**:将大事务分解为多个小事务。 3. **使用索引**:确保查询使用索引,减少范围和持有时间。 4. **避免**:减少需要的数据量,避免行为表。 ### 总结 PostgreSQL 8通过细粒度(特别是8.1后缓冲区的优化)和行减少竞争。解决争用需要监控、优化查询和事务设计。乐观并发控制(OCC)适用于低冲突场景,而高冲突场景更适合机制。 §§相关问题§§ 1. 如何监控PostgreSQL中的等待? 2. PostgreSQL中行和表有什么区别? 3. 在PostgreSQL中如何设置事务隔离别? 4. 乐观并发控制(OCC)在PostgreSQL中如何实现?</think>### PostgreSQL 8 机制与争用解决方案 #### 一、PostgreSQL 8 机制概述 PostgreSQL 8 采用多粒度机制,主要分为两类: 1. **表** (8种模式) $$ \begin{array}{|c|c|c|c|c|c|c|c|c|} \hline \text{模式} & \text{ACCESS SHARE} & \text{ROW SHARE} & \text{ROW EXCLUSIVE} & \text{SHARE UPDATE EXCLUSIVE} & \text{SHARE} & \text{SHARE ROW EXCLUSIVE} & \text{EXCLUSIVE} & \text{ACCESS EXCLUSIVE} \\ \hline \text{冲突矩阵} & ○ & ○ & ● & ● & ● & ● & ● & ● \\ \hline \end{array} $$ - ○: 兼容 ●: 冲突 (如`ACCESS EXCLUSIVE`与所有模式冲突)[^1] 2. **行** ```sql SELECT * FROM table FOR UPDATE; -- 排他 SELECT * FROM table FOR SHARE; -- 共享 ``` 3. **缓冲区优化** PostgreSQL 8.1 重构了缓冲区管理器,将全局`BufMgrLock`拆分为细粒度: - 每个缓冲区页使用独立 - 引入轻量(LWLock)替代系统 - 读写操作并行度提升$300\%$[^1] #### 二、争用检测方法 1. **系统视图监控** ```sql -- 查看阻塞关系 SELECT blocked_locks.pid AS blocked_pid, blocking_locks.pid AS blocking_pid FROM pg_catalog.pg_locks blocked_locks JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid AND blocking_locks.pid != blocked_locks.pid WHERE NOT blocked_locks.granted; ``` 2. **性能指标分析** ```bash # 监控等待时间 pg_stat_statements.max_wait_time pg_stat_activity.wait_event_type = 'Lock' ``` #### 三、争用解决方案 1. **SQL优化策略** - 缩短事务时长:`BEGIN; ... COMMIT;` 尽量紧凑 - 避免长时持有:将`SELECT FOR UPDATE`移至事务最后 - 使用索引覆盖查询:减少范围 2. **参数调优** ```sql ALTER SYSTEM SET deadlock_timeout = '500ms'; -- 死检测间隔 ALTER SYSTEM SET max_locks_per_transaction = 128; -- 增加槽位 ``` 3. **并发控制模型选择** | 机制 | 适用场景 | 示例 | |------|----------|------| | 悲观 (默认) | 高冲突写操作 | 库存扣减 | | **乐观并发控制** | 低冲突场景 | 用户评论更新[^2] | ```sql -- 乐观实现 (使用版本号) UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 100 AND version = current_version; ``` 4. **架构优化** ```mermaid graph LR A[应用层] --> B{读写分离} B --> C[主库-写操作] B --> D[从库-读操作] C --> E[分片1] C --> F[分片2] ``` #### 四、缓冲区专项优化 针对`BufMgrLock`争用问题: 1. **8.1+版本优化方案** - 将全局拆分为$N$个分区,分区数由`NBuffers`决定 - 缓冲区访问哈希分片:$hash(\text{pageID}) \mod N$ 2. **配置建议** ```ini # postgresql.conf shared_buffers = 8GB # 缓冲区大小 effective_io_concurrency = 32 # 并发I/O数 ``` #### 五、争用紧急处理 1. **解命令** ```sql SELECT pg_terminate_backend(pid); -- 终止阻塞进程 ``` 2. **超时机制** ```sql SET lock_timeout = '2s'; -- 单会话超时设置 ``` > **关键建议**:当监控到`pg_stat_activity.wait_event`包含`buffer`时,表明遭遇缓冲区争用,需检查$shared\_buffers$配置和热点数据分布[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值