14 不同对象上的锁
PostgreSQL 18 · 基于俄文 PG18 原版的简体中文译本
14.1. 非关系锁
当需要锁定 PostgreSQL 概念中非 关系 的资源时,会使用类型为 object 的重锁1。此类资源可以是系统目录中几乎任何内容,例如表空间、订阅、模式、角色、策略、枚举类型等。
开始一个事务并在其中创建一张表:
现在查看 pg_locks 中的非关系锁:
此处锁定的资源由三个字段定义:
database — 锁定对象所属数据库的 oid(或为零,表示集群级的通用对象);
classid — pg_class 中的 oid,对应定义资源类型的系统目录表名;
objid — classid 所指向的系统目录表中的 oid。
列 database 显示当前会话连接的数据库 internals。列 classid 指向包含模式的表 pg_namespace。
现在我们可以解析 objid:
因此,public 模式被锁定,以防止在事务完成前被删除。
相应地,删除对象时,会同时对对象本身及其所有依赖对象获取排他锁2。
14.2. 关系扩展锁
当关系中的行数增加时,PostgreSQL 会尽可能在现有页面中利用空闲空间进行插入。但显然,在某个时刻必须添加新页面,即 扩展关系。物理上,页面会添加到相应段文件的末尾(这可能导致创建新的段文件)。
为防止两个进程同时开始添加页面,该操作受到一种特殊的重锁保护,其类型为 extend3。同样,索引清理时也使用这种锁,以防止其他进程在扫描期间添加新页面。
关系扩展锁的行为与之前讨论的锁略有不同:
它在扩展操作完成后立即释放,无需等待事务结束;
它不会导致死锁,因此在检测死锁的程序中对此做了特殊处理:它不会进入等待图。
尽管如此,如果扩展等待时间超过 deadlock_timeout 参数设置的值,检测过程仍会被调用。这是一种异常情况,但当大量并行进程产生高插入负载时可能发生。在这种情况下,检测可能反复触发,实际上使系统陷入瘫痪。
在高插入负载下,扩展锁可能成为瓶颈,因此表文件的扩展不再仅增加一页,而是同时扩展多页(按等待锁的进程数成比例,但每次最多扩展 64 页)4。然而,基于 B-树的索引文件每次仍只增加一页5。(v. 16)
14.3. 页锁
仅在 GIN 索引的特定机制下使用类型为 page 的页面级重锁6。(参见 612)
GIN 索引允许加速在复合值中搜索元素,例如文本文档中的单词。这类索引大致可视为普通 B-树,其中存储的不是文档本身,而是文档中的各个单词。因此,当插入新文档时,索引需要大幅重构,将文档中的每个单词插入其中。
为提升性能,GIN 索引提供了延迟插入功能,通过存储参数 fastupdate 启用。新单词首先快速插入到一个无序的 待处理列表(pending list)中,经过一段时间后,所有累积的单词再批量移入主索引结构。这种优化基于不同文档中单词高度重复的假设,从而节省开销。(默认值:on)
为防止多个进程同时将待处理列表中的单词移入主索引,转移期间会以排他模式锁定索引的元页面。这不会影响索引的常规使用。
与关系扩展锁类似,页面锁在操作完成后立即释放(而非事务结束时),且不会导致死锁。
14.4. 咨询锁
与其他重锁(如关系锁)不同,建议锁 7(advisory locks)从不自动设置,由应用程序开发者手动管理。它们非常适合用于实现非标准的锁定逻辑。
假设存在一个不对应任何数据库对象的资源(我们无法通过 SELECT FOR 或 LOCK TABLE 等命令锁定它)。要锁定该资源,需将其映射为一个数值标识符。若资源具有唯一名称,一种简单方法是使用名称的哈希值:
存在一组完整的函数 8 用于管理建议锁。其名称以 pg_advisory 开头,并包含额外词汇以明确功能:
lock — 获取锁;
try — 尝试获取锁,若无法立即获取则不等待;
unlock — 释放锁;
share — 使用共享模式(默认为排他模式);
xact — 锁定至事务结束(默认为会话结束)。
例如,获取一个会话级的排他锁:
为使锁定机制真正生效,其他进程也必须遵循相同的资源访问顺序。这一规则需由应用程序负责维护。
已设置的锁在事务结束后仍不会被释放:
在完成对资源的操作后,显式释放锁:
14.5. 谓词锁
术语 谓词锁 最初出现在尝试基于锁实现完全隔离的早期尝试中9。当时面临的问题是,即使锁定了所有读取和修改的行,也无法实现完全隔离:表中可能新增满足原有筛选条件的 新行,从而导致 幻影 现象。(参见 51)
因此提出锁定条件(谓词)而非行。当执行带有谓词 a > 10 的查询时,锁定该谓词可防止向表中插入满足该条件的新行,从而避免幻影。但难点在于,当出现另一个条件(如 a < 20)时,需判断两个条件是否重叠。在一般情况下,该问题在算法上不可解;实践中仅能对具有极简单形式的谓词(如示例中)进行处理。
在 PostgreSQL 中,可串行化隔离级别通过另一种机制实现:基于快照的可串行化隔离协议(Serializable Snapshot Isolation, SSI)10。术语 谓词锁 保留了下来,但其含义已根本改变。实际上,此类“锁”并不真正阻塞任何操作,而是用于跟踪事务间的数据依赖关系。
已证明,基于快照的隔离(在 Repeatable Read 级别实现)允许出现 不一致写入异常 和 仅读事务异常,但不会出现其他异常。这两种异常在事务依赖图中对应特定模式,可通过低开销方式检测。(参见 67)
难点在于必须区分两种依赖类型:
一个事务读取了一行,随后被另一个事务修改(RW 依赖);
一个事务修改了一行,随后被另一个事务读取(WR 依赖)。
WR 依赖可通过现有常规锁检测,而 RW 依赖则需借助谓词锁额外跟踪。该跟踪机制在选择可串行化隔离级别时启用,因此至关重要:所有(或至少所有相互关联的)事务必须使用该级别。若任一事务使用其他隔离级别,它将不会设置(或检查)谓词锁,导致可串行化级别退化为 Repeatable Read。
再次强调:尽管名为“锁”,谓词锁实际上并不阻塞任何操作。相反,在事务提交时会进行检查,若发现“不良”依赖结构可能暗示异常,则事务将被中止。
创建一张包含数据的表,并建立一个占用若干页面的索引(为此设置了较低的 fillfactor 值):
若查询通过表的顺序扫描执行,谓词锁将作用于整个表(即使筛选条件仅匹配部分行)。
尽管谓词锁使用独立的基础设施,它们仍显示在 pg_locks 视图中,与重锁一同列出。任何谓词锁始终以一种特殊模式 SIRead(Serializable Isolation Read)获取:
需要注意的是,谓词锁并不总是在事务结束后立即释放,因为它们用于跟踪 事务之间 的依赖关系。但无论如何,其管理是自动完成的。
如果查询是通过索引扫描执行的,情况会向好的方向转变。对于 B-树,只需在读取的表行和索引的叶页面上设置预测性锁即可。这样不仅锁定了特定值,还锁定了整个读取的范围。
覆盖读取范围的叶页面数量可能会发生变化,例如由于插入新行导致索引页面分裂。但实现会考虑这种情况,并锁定新页面:
每读取一个行版本都会创建一个独立的锁,但潜在的行版本数量可能非常多。预测性锁使用单独的锁池,其内存空间在服务器启动时分配。预测性锁的总数受参数 max_pred_locks_per_transaction 和 max_connections 的乘积限制(尽管参数名称暗示按事务分别计数,但实际上并不按事务单独计数)。(默认值:64)(默认值:100)
本质上,这与行锁的问题相同,但解决方式不同:通过提高锁的级别 11。
可以使用配置参数设置提高锁级别时的阈值。如果属于同一页面的行版本锁数量超过 max_pred_locks_per_page,这些锁将被替换为一个页面级别的锁。(默认值:2)
原本有三个 tuple 类型的锁,现在变为一个 page 类型的锁:
同样地,页面锁的级别也会提升。如果属于同一关系的页面锁数量超过 max_pred_locks_per_relation,这些锁将被替换为一个关系级别的锁。(当参数值为负数时,以 max_pred_locks_per_transaction 除以该参数绝对值作为上限;因此默认情况下上限为 32)。(默认值:-2)(默认值:64)
当然,锁级别提升不可避免地会导致更多事务因序列化错误而意外终止。系统吞吐量最终会下降。需要在锁的内存开销与性能之间寻找平衡。
预测性锁支持以下类型的索引:
B-树;
哈希索引、GiST 和 GIN。
如果使用索引访问,但索引不支持预测性锁,则会将整个索引加锁。当然,这也增加了事务因冲突而失败的次数。
为了在可串行化级别更高效地运行,应显式将仅读取的事务标记为 READ ONLY。如果锁管理器确认读取事务与其他事务不可能发生冲突12,它将能够释放已为该事务设置的预测性锁,并不再分配新的锁。如果进一步将此类事务声明为可延迟的(DEFERRABLE),则可避免仅读取事务的异常。(参见 71)
backend/storage/lmgr/lmgr.c, 函数 LockDatabaseObject 和 LockSharedObject. ↩︎
backend/catalog/dependency.c, 函数 performDeletion. ↩︎
backend/storage/lmgr/lmgr.c, 函数 LockRelationForExtension. ↩︎
backend/access/heap/hio.c, 函数 RelationAddBlocks. ↩︎
backend/access/nbtree/nbtpage.c, 函数 _bt_allocbuf. ↩︎
backend/storage/lmgr/lmgr.c, 函数 LockPage. ↩︎
postgrespro.ru/docs/postgresql/18/explicit-locking#ADVISORY-LOCKS. ↩︎
postgrespro.ru/docs/postgresql/18/functions-admin#FUNCTIONS-ADVISORY-LOCKS. ↩︎
K. P. Eswaran, J. N. Gray, R. A. Lorie, I. L. Traiger. The notions of consistency and predicate locks in a database system. ↩︎
backend/storage/lmgr/README-SSI; backend/storage/lmgr/predicate.c. ↩︎
backend/storage/lmgr/predicate.c, 函数 PredicateLockAcquire. ↩︎
backend/storage/lmgr/predicate.c, 宏 SxactIsROSafe. ↩︎