为方便大家交流,建了一个群: 数据库Hacker,欢迎扫码入群
Part III: 锁(Locks)
12. 关系-级锁
12.1 关于锁
锁控制着对共享资源的并发访问。
并发访问意味着多个进程试图在同一时间获得同一个资源。这些进程是并行执行(如果硬件允许)还是在分时模式下按顺序执行都没有区别。如果没有并发访问,就不需要获得锁(例如,共享缓冲区缓存需要锁,而本地缓存可以不需要锁)。
在访问资源之前,进程必须获得该资源上的锁;当操作完成时,必须释放此锁,以使资源对其他进程可用。如果锁是由数据库系统管理的,则自动维护已建立的操作顺序;如果锁由应用程序控制,则协议必须由应用程序本身强制执行。
在较低的级别上,锁只是定义锁状态(无论是否获得它)的共享内存块;它还可以提供一些额外的信息,例如进程号或获取时间。
正如您所猜测的,共享内存段本身就是一种资源。对这类资源的并发访问由操作系统提供的同步原语(如信号量或互斥锁)控制。它们保证了访问共享资源的代码的严格连续执行。在最低级别上,这些原语基于原子CPU指令(如测试和设置或比较和交换)。
一般来说,我们可以使用锁来保护任何资源,只要它可以被明确地识别并分配一个特定的锁地址。
例如,我们可以锁定一个数据库对象,如表(由系统编目中的oid标识)、数据页(由文件名和该文件中的位置标识)、行版本(由页和该页中的偏移量标识)。我们还可以锁定一个内存结构,例如哈希表或缓冲区(由指定的ID标识)。我们甚至可以锁定一个没有物理表示的抽象资源。
但并不总是能够立即获得锁:资源可能已经被其他人锁定。然后进程要么加入队列(如果允许这种特定的锁类型),要么在一段时间后再次尝试。无论哪种方式,它都必须等待锁被释放。
我想挑出两个可以极大地影响锁定效率的因素。
粒度,或者说锁的”粒大小“。如果资源形成层次结构,粒度很重要。
例如,表由页组成,而页又由元组组成。所有这些对象都可以用锁来保护。表级锁是粗粒度的;即使进程需要访问不同的页面或行,它们也禁止并发访问。
行级锁是细粒度的,所以它们没有这个缺点;然而,锁的数量在增加。为了避免为与锁相关的元数据使用过多的内存,PG可以应用各种方法,其中之一是锁升级:如果细粒度锁的数量超过某个阈值,则用单个粗粒度锁替换它们。
可以获得锁的一组模式。
作为一种规则,只应用两种模式。排他模式与所有其他模式不兼容,包括它自己。共享模式允许一个资源同时被多个进程锁定。共享模式用于读取,独占模式用于写入。
一般来说,可能还有其他模式。模式的名称并不重要,重要的是它们的兼容性矩阵。
更细的粒度和对多种兼容模式的支持为并发执行提供了更多的机会。
所有的都可以通过它们的持续时间进行分类。
长期: 获取锁的时间可能很长(在大多数情况下,直到事务结束);它们通常保护关系和行等资源。这些锁通常由PostgreSQL自动管理,但用户仍然可以对这个过程进行一些控制。
长期锁提供多种模式,支持对数据进行各种并发操作。它们通常具有广泛的基础设施(包括诸如等待队列、死锁检测和检测等功能),因为无论如何,它们的维护比对受保护数据的操作要便宜得多。
短期: 锁的获取时间不到一秒,持续时间很少超过几个CPU指令;它们通常保护共享内存中的数据结构。PostgreSQL以完全自动化的方式管理这些锁。
短期锁通常只提供很少的模式和基本的基础设施,这些基础设施可能根本没有工具。
PostgreSQL支持各种类型的锁[1]。重量级锁(在关系和其他对象上获得)和行级锁被认为是长期的。短期锁包括内存结构上的各种锁。此外,还有一组截然不同的断言锁,尽管名为断言锁(predicate locks),但它们根本不是锁。
12.2 重量级锁
重量级锁是长期锁。它们在对象级别获得,主要用于关系,但也可以应用于一些其他类型的对象。重量级锁通常保护对象不被并发更新或禁止在重构期间使用它们,但它们也可以满足其他需求。这样一个模糊的定义是有意为之的:这种类型的锁用于各种目的。它们唯一的共同点是它们的内部结构。
除非另有明确规定,否则术语锁通常意味着重量级锁。
重量级锁位于服务器的共享内存中,可以在pg_locks视图中显示。它们的总数由max_locks_per_transaction值(默认值64)乘以max_connections(默认值100)所限制。
所有事务都使用一个公共的锁池,因此一个事务可以获得多个max_locks_per_transaction锁。真正重要的是,系统中的锁总数不超过定义的限制。由于池是在服务器启动时初始化的,因此更改这两个参数中的任何一个都需要服务器重新启动。
如果资源已经以不兼容的模式锁定,则试图获取另一个锁的进程将加入队列。等待进程不会浪费CPU时间:它们进入睡眠状态,直到锁被释放,操作系统唤醒它们。
两个事务可能会陷入死锁。如果第一个事务在获得被另一个事务锁定的资源之前无法继续其操作,而另一个事务又需要被第一个事务锁定的资源。这种情况相当简单;死锁还可能涉及两个以上的事务。由于死锁会导致无限的等待,PostgreSQL会自动检测它们,并终止其中一个受影响的事务,以确保正常操作可以继续。
不同类型的重量级锁服务于不同的目的,保护不同的资源,支持不同的模式,因此我们将分别考虑它们。下面的列表提供了出现在pg_locks视图locktype列中的锁类型的名称:
transactionid and virtualxid —事务ID上的锁
relation — 关系级锁
tuple — 一个元组上获得的锁
object — 非关系的对象上的锁
extend — 关系扩展上的锁
page — 被一些索引页使用的页级锁
advisory — an advisory lock 建议锁
几乎所有重量级锁都是根据需要自动获取的,并在相应的事务完成时自动释放。不过也有一些例外: 例如,可以显式地设置关系级锁,而建议锁总是由用户管理。
12.3 事务ID锁
每个事务总是拥有自己的排他锁(虚拟的和真实的,如果可用的话)。
PostgreSQL为此提供了两种锁定模式,排他和共享。它们的兼容矩阵非常简单: 共享模式与自身兼容,独占模式不能与任何模式组合。
Shared | Exclusive | |
---|---|---|
Shared | X | |
Exclusive | X | X |
为了跟踪特定事务的完成情况,进程可以在任何模式下请求此事务的锁。由于事务本身已经在自己的ID上持有一个独占锁,因此不可能获得另一个锁。请求此锁的进程加入队列并进入睡眠状态。一旦事务完成,锁被释放,排队的进程被唤醒。显然,它将无法获得锁,因为相应的资源已经消失了,但这个锁实际上并不是所需要的。
让我们在一个单独的会话中启动一个事务,并获得后端的进程ID (PID):
=> BEGIN;
=> SELECT pg_backend_pid();
pg_backend_pid
−−−−−−−−−−−−−−−−
28991
(1 row)
启动的事务持有在它自身虚拟ID上的排它锁:
=> SELECT locktype, virtualxid, mode, granted
FROM pg_locks WHERE pid = 28991;
locktype | virtualxid | mode | granted
−−−−−−−−−−−−+−−−−−−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−
virtualxid | 5/2 | ExclusiveLock | t
(1 row)
这里locktype是锁的类型,virtualxid是虚拟事务ID(标识锁定的资源),mode是锁定模式(在本例中是独占的)。授予标志显示是否已获得请求的锁。
一旦事务获得一个真实的ID,相应的锁被添加到这个列表中:
=> SELECT pg_current_xact_id();
pg_current_xact_id
−−−−−−−−−−−−−−−−−−−−
134971
(1 row)
=> SELECT locktype, virtualxid, transactionid AS xid, mode, granted
FROM pg_locks WHERE pid = 28991;
locktype | virtualxid | xid | mode | granted
−−−−−−−−−−−−−−−+−−−−−−−−−−−−+−−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−
virtualxid | 5/2 | | ExclusiveLock | t
transactionid | | 134971 | ExclusiveLock | t
(2 rows)
现在该事务持有两个ID上的排它锁。
12.4 关系级锁
PG提供了多达8种关系级锁[2](表、索引及其它关系类型)。这样的多样性允许您最大化可以在一个关系上运行的并发命令的数量。
下一页显示了兼容性矩阵,其中包含了需要相应锁定模式的命令示例。记住所有这些模式或试图找到它们命名背后的逻辑是没有意义的,但查看这些数据、得出一些一般性结论并根据需要参考这个表绝对是有用的。