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考试咨询
PostgreSQL 锁 之 关系级锁的更多相关文章
- MySQL 全局锁、表级锁、行级锁,你搞清楚了吗?
大家好,我是小林. 最近重新补充了<MySQL 有哪些锁>文章内容: 增加记录锁.间隙锁.net-key 锁 增加插入意向锁 增加自增锁为 innodb_autoinc_lock_mode ...
- MySQL行级锁、表级锁、页级锁详细介绍
原文链接:http://www.jb51.net/article/50047.htm 页级:引擎 BDB.表级:引擎 MyISAM , 理解为锁住整个表,可以同时读,写不行行级:引擎 INNODB , ...
- MySQL行级锁和表级锁
锁定用于确保事务完整性和数据库一致性. 锁定可以防止用户读取其他用户正在更改的数据,并防止多个用户同时更改相同的数据. 如果不使用锁定,数据库中的数据可能在逻辑上变得不正确,而针对这些数据进行查询可能 ...
- Mysql 的表级锁和行级锁
表级锁 MySQL表级锁分为读锁和写锁. 读锁 用法:LOCK TABLE table_name [ AS alias_name ] READ 释放锁使用UNLOCK tables.可以为表使用别名, ...
- Mysql的行级锁与表级锁
在计算机科学中,锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足. 在DBMS中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎).表级锁(MYISAM ...
- Mysql的表级锁和行级锁
表级锁 MySQL表级锁分为读锁和写锁. 读锁 用法:LOCK TABLE table_name [ AS alias_name ] READ 释放锁使用UNLOCK tables.可以为表使用别名, ...
- MySQL的中的全局锁、表级锁、行锁
MySQL的中的全局锁.表级锁.行锁 学习极客时间-林晓彬老师-MySQL实战45讲 学习整理 全局锁 对整个数据库实例加锁.通过使用Flush tables with read lock (FTWR ...
- MySQL中的全局锁和表级锁
全局锁和表锁 数据库锁设计的初衷是解决并发出现的一些问题.当出现并发访问的时候,数据库需要合理的控制资源的访问规则.而锁就是访问规则的重要数据结构. 根据锁的范围,分为全局锁.表级锁和行级锁三类. 全 ...
- 详述 MySQL 中的行级锁、表级锁和页级锁
转自:https://blog.csdn.net/qq_35246620/article/details/69943011 refer:cnblogs.com/f-ck-need-u/p/899547 ...
随机推荐
- Java思考——HashSet集合如何保证元素的唯一性也就是不包含重复元素?
首先将源码逐级找出来1.HashSet<String> hs=new HashSet<String>(); hs.add("hello"); ...
- c#的委托实例
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
- 如何快速开发基于Winform的应用系统
在我们实际业务开发中,从头开发一个应用系统,不管是基于BS的前端项目,还是基于Winform的CS应用系统,都是由容易到复杂,逐步演化的一个开发过程,如果我们基于一定基础上,并配合一些配套的开发工具, ...
- 学习 Haproxy (一)
haproxy是一个开源的.高性能的基于tcp和http应用代理的HA的.LB服务软件,它支持双机热备.HA.LB.虚拟主机.图形界面查看状态信息等功能,其配置简单.维护方便,而且后端RS的healt ...
- Lambda8 表达式
Lambda 表达式 Lambda 表达式是 JDK8 的一个新特性,可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构. JDK 也提 ...
- c++中的赋值函数
在c++中,对于任意一个类Class A,如果程序员不显示的声明和定义上述函数,C++编译器将会自动的为A产生4个public inline 的默认函数,这4个函数最常见的形式为: A() //默认构 ...
- Asp.Net Core之Identity应用(上篇)
一.前言 在前面的篇章介绍中,简单介绍了IdentityServer4持久化存储机制相关配置和操作数据,实现了数据迁移,但是未对用户实现持久化操作说明.在总结中我们也提到了, 因为IdentitySe ...
- 实用的 CSS — 贝塞尔曲线(cubic-bezier)
欢迎移步我的博客阅读:<实用的 CSS - 贝塞尔曲线(cubic-bezier)> 前言 在了解 cubic-bezier 之前,你需要对 CSS3 中的动画效果有所认识,它是 anim ...
- 一个关于小程序与单片机的通信实例(TCP/IP)
前言 这是一个18年初的创业项目的核心功能要求,我们当时打算做一个共享类的项目,项目的主题是共享图书,线下的形式租借图书,我们当时是考虑做一个借书柜的形式,然后线下生产投放借书柜,这些借书柜本身能存放 ...
- Hibernate快速上手
一. Hibernate介绍 1. Hibernate简介 Hibernate是一个开放源码的对象-关系映射(ORM)框架,他对JDBC进行了轻量级封装,开发人员可以使用面向对象的编程思想来进行持久层 ...