# 不同对象上的锁

LLMS 索引： [llms.txt](/zh18/llms.txt)

---

<span id="internal-ru-chapter-14" class="pdf-anchor"></span><span id="internal-ru-page-294" class="pdf-anchor"></span>

<span id="internal-ru-dummy-102" class="pdf-anchor"></span>

## 14.1. 非关系锁 {#internal-ru-section-14-1}

<span id="internal-ru-dummy-103" class="pdf-anchor"></span>

当需要锁定 PostgreSQL 概念中非 *关系* 的资源时，会使用类型为 `object` 的重锁[^internal-ru-294-1]。此类资源可以是系统目录中几乎任何内容，例如表空间、订阅、模式、角色、策略、枚举类型等。

开始一个事务并在其中创建一张表：

```text {label="psql"}
=> BEGIN;
=> CREATE TABLE example(n integer);
```

现在查看 `pg_locks` 中的非关系锁：

<span id="internal-ru-page-295" class="pdf-anchor"></span>

```text {label="psql"}
=> SELECT database,
  (
    SELECT datname FROM pg_database WHERE oid = database
  ) AS dbname,
  classid,
  (
    SELECT relname FROM pg_class WHERE oid = classid
  ) AS classname,
  objid,
  mode,
  granted
FROM pg_locks
WHERE locktype = 'object'
  AND pid = pg_backend_pid() \gx
-[ RECORD 1 ]--------------
database  | 16391
dbname    | internals
classid   | 2615
classname | pg_namespace
objid     | 2200
mode      | AccessShareLock
granted   | t
```

此处锁定的资源由三个字段定义：

**database** — 锁定对象所属数据库的 `oid`（或为零，表示集群级的通用对象）；

**classid** — `pg_class` 中的 `oid`，对应定义资源类型的系统目录表名；

**objid** — `classid` 所指向的系统目录表中的 `oid`。

列 `database` 显示当前会话连接的数据库 `internals`。列 `classid` 指向包含模式的表 `pg_namespace`。

现在我们可以解析 `objid`：

```text {label="psql"}
=> SELECT nspname FROM pg_namespace WHERE oid = 2200;
 nspname
---------
 public
(1 row)
```

因此，`public` 模式被锁定，以防止在事务完成前被删除。

相应地，删除对象时，会同时对对象本身及其所有依赖对象获取排他锁[^internal-ru-295-1]。

```text {label="psql"}
=> ROLLBACK;
```

<span id="internal-ru-page-296" class="pdf-anchor"></span>

## 14.2. 关系扩展锁 {#internal-ru-section-14-2}

<span id="internal-ru-dummy-104" class="pdf-anchor"></span>

当关系中的行数增加时，PostgreSQL 会尽可能在现有页面中利用空闲空间进行插入。但显然，在某个时刻必须添加新页面，即 *扩展关系*。物理上，页面会添加到相应段文件的末尾（这可能导致创建新的段文件）。

为防止两个进程同时开始添加页面，该操作受到一种特殊的重锁保护，其类型为 `extend`[^internal-ru-296-1]。同样，索引清理时也使用这种锁，以防止其他进程在扫描期间添加新页面。

关系扩展锁的行为与之前讨论的锁略有不同：

- 它在扩展操作完成后立即释放，无需等待事务结束；

- 它不会导致死锁，因此在检测死锁的程序中对此做了特殊处理：它不会进入等待图。

> [!NOTE]
> 尽管如此，如果扩展等待时间超过 *deadlock\_timeout* 参数设置的值，检测过程仍会被调用。这是一种异常情况，但当大量并行进程产生高插入负载时可能发生。在这种情况下，检测可能反复触发，实际上使系统陷入瘫痪。

在高插入负载下，扩展锁可能成为瓶颈，因此表文件的扩展不再仅增加一页，而是同时扩展多页（按等待锁的进程数成比例，但每次最多扩展 64 页）[^internal-ru-296-2]。然而，基于 B-树的索引文件每次仍只增加一页[^internal-ru-296-3]。（v. 16）

<span id="internal-ru-page-297" class="pdf-anchor"></span>

## 14.3. 页锁 {#internal-ru-section-14-3}

<span id="internal-ru-dummy-105" class="pdf-anchor"></span>

仅在 GIN 索引的特定机制下使用类型为 `page` 的页面级重锁[^internal-ru-297-1]。（参见 [612](/zh18/28-gin/#internal-ru-page-613)）

GIN 索引允许加速在复合值中搜索元素，例如文本文档中的单词。这类索引大致可视为普通 B-树，其中存储的不是文档本身，而是文档中的各个单词。因此，当插入新文档时，索引需要大幅重构，将文档中的每个单词插入其中。

为提升性能，GIN 索引提供了延迟插入功能，通过存储参数 *fastupdate* 启用。新单词首先快速插入到一个无序的 *待处理列表*（pending list）中，经过一段时间后，所有累积的单词再批量移入主索引结构。这种优化基于不同文档中单词高度重复的假设，从而节省开销。（默认值：`on`）

为防止多个进程同时将待处理列表中的单词移入主索引，转移期间会以排他模式锁定索引的元页面。这不会影响索引的常规使用。

与关系扩展锁类似，页面锁在操作完成后立即释放（而非事务结束时），且不会导致死锁。

## 14.4. 咨询锁 {#internal-ru-section-14-4}

<span id="internal-ru-dummy-106" class="pdf-anchor"></span><span id="internal-ru-page-298" class="pdf-anchor"></span>

与其他重锁（如关系锁）不同，*建议锁* [^internal-ru-297-2]（advisory locks）从不自动设置，由应用程序开发者手动管理。它们非常适合用于实现非标准的锁定逻辑。

假设存在一个不对应任何数据库对象的资源（我们无法通过 `SELECT FOR` 或 `LOCK TABLE` 等命令锁定它）。要锁定该资源，需将其映射为一个数值标识符。若资源具有唯一名称，一种简单方法是使用名称的哈希值：

```text {label="psql"}
=> SELECT hashtext('ресурс1');
 hashtext
-----------
 243773337
(1 row)
```

存在一组完整的函数 [^internal-ru-298-1] 用于管理建议锁。其名称以 `pg_advisory` 开头，并包含额外词汇以明确功能：

**lock** — 获取锁；

**try** — 尝试获取锁，若无法立即获取则不等待；

**unlock** — 释放锁；

**share** — 使用共享模式（默认为排他模式）；

**xact** — 锁定至事务结束（默认为会话结束）。

例如，获取一个会话级的排他锁：

```text {label="psql"}
=> BEGIN;
=> SELECT pg_advisory_lock(hashtext('ресурс1'));
=> SELECT locktype, objid, mode, granted
FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid();
 locktype |   objid   |     mode      | granted
----------+-----------+---------------+---------
 advisory | 243773337 | ExclusiveLock | t
(1 row)
```

<span id="internal-ru-page-299" class="pdf-anchor"></span>

为使锁定机制真正生效，其他进程也必须遵循相同的资源访问顺序。这一规则需由应用程序负责维护。

已设置的锁在事务结束后仍不会被释放：

```text {label="psql"}
=> COMMIT;
=> SELECT locktype, objid, mode, granted
FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid();
 locktype |   objid   |     mode      | granted
----------+-----------+---------------+---------
 advisory | 243773337 | ExclusiveLock | t
(1 row)
```

在完成对资源的操作后，显式释放锁：

```text {label="psql"}
=> SELECT pg_advisory_unlock(hashtext('ресурс1'));
```

## 14.5. 谓词锁 {#internal-ru-section-14-5}

<span id="internal-ru-dummy-107" class="pdf-anchor"></span>

术语 *谓词锁* 最初出现在尝试基于锁实现完全隔离的早期尝试中[^internal-ru-299-1]。当时面临的问题是，即使锁定了所有读取和修改的行，也无法实现完全隔离：表中可能新增满足原有筛选条件的 *新行*，从而导致 *幻影* 现象。（参见 [51](/zh18/02-isolation/#internal-ru-page-52)）

因此提出锁定条件（谓词）而非行。当执行带有谓词 `a > 10` 的查询时，锁定该谓词可防止向表中插入满足该条件的新行，从而避免幻影。但难点在于，当出现另一个条件（如 `a < 20`）时，需判断两个条件是否重叠。在一般情况下，该问题在算法上不可解；实践中仅能对具有极简单形式的谓词（如示例中）进行处理。

<span id="internal-ru-page-300" class="pdf-anchor"></span>

在 PostgreSQL 中，可串行化隔离级别通过另一种机制实现：基于快照的可串行化隔离协议（Serializable Snapshot Isolation, SSI）[^internal-ru-300-1]。术语 *谓词锁* 保留了下来，但其含义已根本改变。实际上，此类“锁”并不真正阻塞任何操作，而是用于跟踪事务间的数据依赖关系。

已证明，基于快照的隔离（在 Repeatable Read 级别实现）允许出现 *不一致写入异常* 和 *仅读事务异常*，但不会出现其他异常。这两种异常在事务依赖图中对应特定模式，可通过低开销方式检测。（参见 [67](/zh18/02-isolation/#internal-ru-page-68)）

难点在于必须区分两种依赖类型：

- 一个事务读取了一行，随后被另一个事务修改（RW 依赖）；

- 一个事务修改了一行，随后被另一个事务读取（WR 依赖）。

WR 依赖可通过现有常规锁检测，而 RW 依赖则需借助谓词锁额外跟踪。该跟踪机制在选择可串行化隔离级别时启用，因此至关重要：*所有*（或至少所有相互关联的）事务必须使用该级别。若任一事务使用其他隔离级别，它将不会设置（或检查）谓词锁，导致可串行化级别退化为 Repeatable Read。

再次强调：尽管名为“锁”，谓词锁实际上并不阻塞任何操作。相反，在事务提交时会进行检查，若发现“不良”依赖结构可能暗示异常，则事务将被中止。

<span id="internal-ru-page-301" class="pdf-anchor"></span>

创建一张包含数据的表，并建立一个占用若干页面的索引（为此设置了较低的 *fillfactor* 值）：

```text {label="psql"}
=> CREATE TABLE pred(n numeric, s text);
=> INSERT INTO pred(n) SELECT n FROM generate_series(1,10000) n;
=> CREATE INDEX ON pred(n) WITH (fillfactor = 10);
=> ANALYZE pred;
```

若查询通过表的顺序扫描执行，谓词锁将作用于整个表（即使筛选条件仅匹配部分行）。

```text {label="psql" hl_lines="11"}
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
          70273
(1 row)
=> BEGIN ISOLATION LEVEL SERIALIZABLE;
=> EXPLAIN (analyze, costs off, timing off, summary off, buffers off)
  SELECT * FROM pred WHERE n > 100;
                   QUERY PLAN
------------------------------------------------
 Seq Scan on pred (actual rows=9900.00 loops=1)
   Filter: (n > '100'::numeric)
   Rows Removed by Filter: 100
(3 rows)
```

尽管谓词锁使用独立的基础设施，它们仍显示在 `pg_locks` 视图中，与重锁一同列出。任何谓词锁始终以一种特殊模式 SIRead（Serializable Isolation Read）获取：

<span id="internal-ru-page-302" class="pdf-anchor"></span>

```text {label="psql"}
=> SELECT relation::regclass, locktype, page, tuple
FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 70273
ORDER BY 1, 2, 3, 4;
 relation | locktype | page | tuple
----------+----------+------+-------
 pred     | relation |      |
(1 row)
    => ROLLBACK;
```

<span id="internal-ru-dummy-108" class="pdf-anchor"></span>

需要注意的是，谓词锁并不总是在事务结束后立即释放，因为它们用于跟踪 *事务之间* 的依赖关系。但无论如何，其管理是自动完成的。

如果查询是通过索引扫描执行的，情况会向好的方向转变。对于 B-树，只需在读取的表行和索引的叶页面上设置预测性锁即可。这样不仅锁定了特定值，还锁定了整个读取的范围。

```text {label="psql" hl_lines="6"}
=> BEGIN ISOLATION LEVEL SERIALIZABLE;
=> EXPLAIN (analyze, costs off, timing off, summary off, buffers off)
  SELECT * FROM pred WHERE n BETWEEN 1000 AND 1001;
                            QUERY PLAN
-------------------------------------------------------------------
 Index Scan using pred_n_idx on pred (actual rows=2.00 loops=1)
   Index Cond: ((n >= '1000'::numeric) AND (n <= '1001'::numeric))
   Index Searches: 1
(3 rows)
```

```text {label="psql"}
=> SELECT relation::regclass, locktype, page, tuple
FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 70273
ORDER BY 1, 2, 3, 4;
  relation  | locktype | page | tuple
------------+----------+------+-------
 pred       | tuple    |    4 |    96
 pred       | tuple    |    4 |    97
 pred_n_idx | page     |   28 |
(3 rows)
```

覆盖读取范围的叶页面数量可能会发生变化，例如由于插入新行导致索引页面分裂。但实现会考虑这种情况，并锁定新页面：

<span id="internal-ru-page-303" class="pdf-anchor"></span>

```text {label="psql" hl_lines="11 12 13 14"}
=> INSERT INTO pred
  SELECT 1000+(n/1000.0) FROM generate_series(1,999) n;
=> SELECT relation::regclass, locktype, page, tuple
FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 70273
ORDER BY 1, 2, 3, 4;
  relation  | locktype | page | tuple
------------+----------+------+-------
 pred       | tuple    |    4 |    96
 pred       | tuple    |    4 |    97
 pred_n_idx | page     |   28 |
 pred_n_idx | page     |  266 |
 pred_n_idx | page     |  267 |
 pred_n_idx | page     |  268 |
 pred_n_idx | page     |  269 |
(7 rows)
```

每读取一个行版本都会创建一个独立的锁，但潜在的行版本数量可能非常多。预测性锁使用单独的锁池，其内存空间在服务器启动时分配。预测性锁的总数受参数 *max_pred_locks_per_transaction* 和 *max_connections* 的乘积限制（尽管参数名称暗示按事务分别计数，但实际上并不按事务单独计数）。（默认值：`64`）（默认值：`100`）

本质上，这与行锁的问题相同，但解决方式不同：通过提高锁的级别 [^internal-ru-303-1]。

可以使用配置参数设置提高锁级别时的阈值。如果属于同一页面的行版本锁数量超过 *max_pred_locks_per_page*，这些锁将被替换为一个页面级别的锁。（默认值：`2`）

```text {label="psql" hl_lines="5"}
=> EXPLAIN (analyze, costs off, timing off, summary off, buffers off)
  SELECT * FROM pred WHERE n BETWEEN 1000 AND 1002;
                            QUERY PLAN
-------------------------------------------------------------------
 Index Scan using pred_n_idx on pred (actual rows=3.00 loops=1)
   Index Cond: ((n >= '1000'::numeric) AND (n <= '1002'::numeric))
   Index Searches: 1
(3 rows)
```

<span id="internal-ru-page-304" class="pdf-anchor"></span>

原本有三个 `tuple` 类型的锁，现在变为一个 `page` 类型的锁：

```text {label="psql" hl_lines="6"}
=> SELECT relation::regclass, locktype, page, tuple
FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 70273
ORDER BY 1, 2, 3, 4;
  relation  | locktype | page | tuple
------------+----------+------+-------
 pred       | page     |    4 |
 pred_n_idx | page     |   28 |
 pred_n_idx | page     |  266 |
 pred_n_idx | page     |  267 |
 pred_n_idx | page     |  268 |
 pred_n_idx | page     |  269 |
(6 rows)
```

```text {label="psql"}
=> ROLLBACK;
```

同样地，页面锁的级别也会提升。如果属于同一关系的页面锁数量超过 *max_pred_locks_per_relation*，这些锁将被替换为一个关系级别的锁。（当参数值为负数时，以 *max_pred_locks_per_transaction* 除以该参数绝对值作为上限；因此默认情况下上限为 32）。（默认值：`-2`）（默认值：`64`）

当然，锁级别提升不可避免地会导致更多事务因序列化错误而意外终止。系统吞吐量最终会下降。需要在锁的内存开销与性能之间寻找平衡。

预测性锁支持以下类型的索引：

- B-树；

- 哈希索引、GiST 和 GIN。

如果使用索引访问，但索引不支持预测性锁，则会将整个索引加锁。当然，这也增加了事务因冲突而失败的次数。

<span id="internal-ru-page-305" class="pdf-anchor"></span>

为了在可串行化级别更高效地运行，应显式将仅读取的事务标记为 `READ ONLY`。如果锁管理器确认读取事务与其他事务不可能发生冲突[^internal-ru-305-1]，它将能够释放已为该事务设置的预测性锁，并不再分配新的锁。如果进一步将此类事务声明为可延迟的（`DEFERRABLE`），则可避免仅读取事务的异常。（参见 [71](/zh18/02-isolation/#internal-ru-page-72)）

[^internal-ru-294-1]: [backend/storage/lmgr/lmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/lmgr.c;hb=REL_18_STABLE>), 函数 LockDatabaseObject 和 LockSharedObject.
[^internal-ru-295-1]: [backend/catalog/dependency.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/catalog/dependency.c;hb=REL_18_STABLE>), 函数 performDeletion.
[^internal-ru-296-1]: [backend/storage/lmgr/lmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/lmgr.c;hb=REL_18_STABLE>), 函数 LockRelationForExtension.
[^internal-ru-296-2]: [backend/access/heap/hio.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/hio.c;hb=REL_18_STABLE>), 函数 RelationAddBlocks.
[^internal-ru-296-3]: [backend/access/nbtree/nbtpage.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/nbtree/nbtpage.c;hb=REL_18_STABLE>), 函数 \_bt\_allocbuf.
[^internal-ru-297-1]: [backend/storage/lmgr/lmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/lmgr.c;hb=REL_18_STABLE>), 函数 LockPage.
[^internal-ru-297-2]: [postgrespro.ru/docs/postgresql/18/explicit-locking#ADVISORY-LOCKS](<https://postgrespro.ru/docs/postgresql/18/explicit-locking#ADVISORY-LOCKS>).
[^internal-ru-298-1]: [postgrespro.ru/docs/postgresql/18/functions-admin#FUNCTIONS-ADVISORY-LOCKS](<https://postgrespro.ru/docs/postgresql/18/functions-admin#FUNCTIONS-ADVISORY-LOCKS>).
[^internal-ru-299-1]: [K.](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [P.](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [Eswaran,](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [J.](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [N.](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [Gray,](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [R.](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [A.](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [Lorie,](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [I.](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [L.](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [Traiger](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>)[.](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [The](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [notions](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [of](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [consistency](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [and](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [predicate](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [locks](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [in](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [a](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [database](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>) [system](<https://jimgray.azurewebsites.net/papers/on%20the%20notions%20of%20consistency%20and%20predicate%20locks%20in%20a%20database%20system%20cacm.pdf>).
[^internal-ru-300-1]: [backend/storage/lmgr/README-SSI](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/README-SSI;hb=REL_18_STABLE>); [backend/storage/lmgr/predicate.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/predicate.c;hb=REL_18_STABLE>).
[^internal-ru-303-1]: [backend/storage/lmgr/predicate.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/predicate.c;hb=REL_18_STABLE>), 函数 PredicateLockAcquire.
[^internal-ru-305-1]: [backend/storage/lmgr/predicate.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/predicate.c;hb=REL_18_STABLE>), 宏 SxactIsROSafe.
