# 多样的锁

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

---

## 14.1 非对象锁 {#141-非对象锁}

为了锁定不被视为&quot;关系&quot;的资源，PostgreSQL 使用 object 类型 [^zh-14-miscellaneous-locks-1] 的重锁。你几乎可以锁定存储在系统表中的任何东西：表空间、订阅、模式、角色、策略、枚举数据类型等等。

让我们开启一个事务创建一张表：

```text
=> BEGIN;
=> CREATE TABLE example(n integer);
```




现在让我们看看 pg\_locks 表中的非关系锁：

```text
=> 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
```




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

<strong>database</strong> — 包含被锁定对象的数据库 oid (如果此对象是整个集簇共有的，则为零)

<strong>classid</strong> — pg\_class 中列出的 oid，对应于定义资源类型的系统目录表的名称

<strong>objid</strong> — 系统目录表中列出的 oid，被 classid 所引用

database 值指向 internals 数据库；它是当前会话所连接的数据库。classid 列指向 pg\_namespace 表，该表列出了模式。

现在我们可以解读 objid 了：

```text
=> SELECT nspname FROM pg_namespace WHERE oid = 2200;
 nspname
−−−−−−−−−
 public
(1 row)
```




所以，PostgreSQL 已经锁定了 public 模式，以确保事务仍在运行时没有人可以删除它。

类似的，对象删除需要获取对象本身及其依赖的所有资源的独占锁 [^zh-14-miscellaneous-locks-2]。

```text
=> ROLLBACK;
```




## 14.2 关系扩展锁 {#142-关系扩展锁}

随着关系中元组数量的增长，PostgreSQL 会尽可能将新元组插入到现有页面的空闲空间中。但很明显，在某些时候，它将不得不添加新的页面，即扩展表。就物理布局而言，新的页面会被添加到相应文件的末尾 (这会导致创建一个新的文件)。

为了使新页面一次只能由一个进程添加，这个操作受到一种特殊的 extend 类型 [^zh-14-miscellaneous-locks-3] 的重锁保护。索引清理也使用这种锁来禁止在索引扫描期间添加新的页面。

关系扩展锁的行为与我们迄今为止看到的有些许不同：

- 一旦扩展完成，它们便会被立即释放，无需等待事务完成。

- 它们不会导致死锁，所以不包含在等待图中。
  
  
  
  
  然而，如果扩展关系的过程超过了 <em>deadlock\_timeout</em>，仍会执行死锁检测。这不是一种典型的情况，但如果大量进程同时执行多个插入操作，就可能会发生。在这种情况下，会多次调用死锁检测，这实际上会导致正常的系统操作瘫痪。
  
  
  
  
  为了将此风险降至最低，堆文件会一次性扩展多个页面<span class="book-margin-note" aria-label="原书旁注">v. 9.6</span> (与等待锁的进程数成比例，但每次操作不超过 512 个页面) [^zh-14-miscellaneous-locks-4]。例外的是 B 树索引文件，一次只扩展一个页面 [^zh-14-miscellaneous-locks-5]。

## 14.3 页锁 {#143-页锁}

page 类型的页级重锁 [^zh-14-miscellaneous-locks-6] 仅应用于 GIN 索引，并且仅在以下情况使用。

GIN 索引可以加速复合值中元素的搜索，例如文本文档中的单词。它们大致可以描述为存储单独的单词而不是整个文档的 B 树。当添加新的文档时，必须彻底更新索引，以包含此文档中出现的每个单词。

为了提高性能，GIN 索引允许延迟插入，由 <em>fastupdate</em><span class="book-margin-note" aria-label="原书旁注">on</span> 存储参数控制。新单词首先会被快速添加到一个无序的 <em>pending list</em> 中，一段时间之后，所有累积的条目都会被移动到主索引结构中。由于不同的文档可能包含重复的单词，因此这种方式无疑是十分划算的。

为了避免多个进程同时转移单词，索引的元页面会以独占模式锁定，直到所有的单词从 <em>pending list</em> 中移动到主索引。这个锁不会干扰正常的索引使用。

就像关系扩展锁一样，页锁在任务完成后立即释放，无需等待事务结束，因此它们永远不会导致死锁。

## 14.4 咨询锁 {#144-咨询锁}

与其他重锁 (如关系锁) 不同，咨询锁 [^zh-14-miscellaneous-locks-7] 永远不会自动获取：它们由应用开发人员控制。如果应用程序出于某些特定目的需要使用专门的锁逻辑，这些锁就很方便使用。

假设我们需要锁定一个不与任何数据库对象对应的资源 (我们可以使用 SELECT FOR 或 LOCK TABLE 命令锁定的资源)。在这种情况下，需要为此资源分配一个数字 ID 。如果该资源有一个唯一的名称，那么最简单的方式是为此名称生成一个哈希码：

```text
=> SELECT hashtext('resource1');
 hashtext
−−−−−−−−−−−
 991601810
(1 row)
```




PostgreSQL 提供了一整套用于管理咨询锁的函数 [^zh-14-miscellaneous-locks-8]。它们的名称以 pg\_advisory 前缀开头，并且包含以下暗示函数用途的单词：

<strong>lock</strong> — 获取锁

<strong>try</strong> — 如果可以无需等待，便获取锁

<strong>unlock</strong> — 释放锁

<strong>share</strong> — 使用共享锁模式 (默认情况下，使用独占模式)

<strong>xact</strong> — 获取并持有锁直至事务结束 (默认情况下，锁会持有至会话结束)

让我们获取一个独占锁，直至会话结束：

```text
=> BEGIN;
=> SELECT pg_advisory_lock(hashtext('resource1'));
=> SELECT locktype, objid, mode, granted
FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid();
 locktype |   objid   |     mode      | granted
−−−−−−−−−−+−−−−−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−
 advisory | 991601810 | ExclusiveLock | t
(1 row)
```




为了确保咨询锁实际起作用，其他进程在访问资源时也必须遵守既定的顺序；这必须由应用程序保证。

即使在事务完成之后，获取的锁也会继续保持：

```text
=> COMMIT;
=> SELECT locktype, objid, mode, granted
FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid();
 locktype |   objid   |     mode      | granted
−−−−−−−−−−+−−−−−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−
 advisory | 991601810 | ExclusiveLock | t
(1 row)
```




一旦对资源的操作结束，锁必须被显式释放：

```text
=> SELECT pg_advisory_unlock(hashtext('resource1'));
```




## 14.5 谓词锁 {#145-谓词锁}

谓词锁这个术语最早出现在首次尝试实现基于锁的完全隔离的时候 [^zh-14-miscellaneous-locks-9]。当时面临的问题是，即使锁定了要读取和更新的所有行，仍然无法保证完全隔离。实际上，如果新插入的行满足过滤条件，它们将成为幻象。

因此，有人建议锁定条件 (谓词) 而不是行。如果使用 a &gt; 10 的谓词运行查询，锁定这个谓词将不允许在满足此条件的情况下向表中添加新行，因此可以避免幻象的出现。麻烦的是，如果出现带有不同谓词的查询，例如 a &lt; 20，你必须找出这些谓词是否重叠。理论上，这个问题在算法上是无解的；实际上，仅仅是非常简单的谓词类别才能解决 (如本例所示)。

在 PostgreSQL 中，可串行化隔离级别以不同的方式实现：它使用可串行化快照隔离 (SSI) 协议 [^zh-14-miscellaneous-locks-10]。谓词锁这个术语仍然存在，但其含义已经彻底变了。事实上，这种&quot;锁&quot;并不锁定任何东西：它们用于跟踪不同事务之间的数据依赖关系。

已经证明，可重复读级别的快照隔离除了写偏序和只读事务异常之外，不允许任何异常。这两种异常会导致数据依赖图中的特定模式，这些模式可以以相对较低的成本发现。

问题是我们必须区分两种类型的依赖关系：

- 第一个事务读取了之后由第二个事务更新的行 (RW 依赖)。

- 第一个事务修改了之后由第二个事务读取的行 (WR 依赖)。

WR 依赖可以使用常规锁来检测，但是 RW 依赖必须通过谓词锁来跟踪。这种跟踪在可串行化隔离级别下会自动开启，这也正是为什么将该级别用于所有事务 (或至少所有相互关联的事务) 的重要原因。如果任何事务在不同级别下运行，它将不会设置 (或检查) 谓词锁，因此可串行化级别将降级为可重复读。

我想再次强调，尽管它们的名字如此，但谓词锁不会锁定任何东西。相反，当一个事务即将提交时，会检查&quot;危险&quot;的依赖关系，如果 PostgreSQL 怀疑有异常，这个事务将被中止。

让我们创建一个带有索引的表，该索引包括多个页面 (可以通过使用一个较低的 fillfactor 值实现)：

```text
=> 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
> => SELECT pg_backend_pid();
>  pg_backend_pid
> −−−−−−−−−−−−−−−−
>           34753
> (1 row)
> => BEGIN ISOLATION LEVEL SERIALIZABLE;
> => EXPLAIN (analyze, costs off, timing off, summary off)
> SELECT * FROM pred WHERE n > 100;
>                QUERY PLAN
> −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
>  Seq Scan on pred (actual rows=9900 loops=1)
>    Filter: (n > '100'::numeric)
>    Rows Removed by Filter: 100
> (3 rows)
> ```

虽然谓词锁有它们自己的基础设施，但 pg\_locks 视图将它们与重锁显示在一起。所有的谓词锁总是以 SIRead 模式获取，这表示可串行化隔离读：

```text
=> SELECT relation::regclass, locktype, page, tuple
FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 34753
ORDER BY 1, 2, 3, 4;
 relation | locktype | page | tuple
−−−−−−−−−−+−−−−−−−−−−+−−−−−−+−−−−−−−
 pred     | relation |      |
(1 row)
```




> ```text
> => ROLLBACK;
> ```

注意，谓词锁的持续时间可能比事务本身更长，因为它们用于跟踪事务之间的依赖关系。但无论如何，这些锁是自动管理的。

如果查询执行索引扫描，情况就会有所改善。对于 B 树索引，只需在读取的堆元组和扫描过的索引叶子页面上设置谓词锁。它将&quot;锁定&quot;已读取的整个范围，而不仅仅是确切的值。

> ```text
> => BEGIN ISOLATION LEVEL SERIALIZABLE;
> => EXPLAIN (analyze, costs off, timing off, summary off)
> SELECT * FROM pred WHERE n BETWEEN 1000 AND 1001;
>                           QUERY PLAN
> −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
>  Index Scan using pred_n_idx on pred (actual rows=2 loops=1)
>    Index Cond: ((n >= '1000'::numeric) AND (n <= '1001'::numeric))
> (2 rows)
> ```

```text
=> SELECT relation::regclass, locktype, page, tuple
FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 34753
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)
```




与已扫描元组相对应的叶子页面数量可以发生改变：例如，当向表中插入新行时，索引页面可能会发生分裂。然而，PostgreSQL 会将其考虑在内，并且也会锁定新出现的页面：

```text
=> 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 = 34753
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)
```




每个读取的元组都会被单独锁定，并且可能有相当多这样的元组。谓词锁使用它们自己的锁池，这个锁池在服务器启动时分配。谓词锁的总数受到 <em>max\_pred\_locks\_per\_transaction</em><span class="book-margin-note" aria-label="原书旁注">64</span> 乘以 <em>max\_connections</em><span class="book-margin-note" aria-label="原书旁注">100</span> 的限制 (尽管参数名如此，谓词锁并不是按单独事务计数的)。

在这里，我们遇到了与行级锁相同的问题，但解决方式不同：应用了锁升级。[^zh-14-miscellaneous-locks-11]

一旦与单个页面相关的元组锁的数量超过了 <em>max\_pred\_locks\_per\_page</em><span class="book-margin-note" aria-label="原书旁注">2</span> 参数的值，便会被一个单独的页级锁替代<span class="book-margin-note" aria-label="原书旁注">v. 10</span>。

> ```text
> => EXPLAIN (analyze, costs off, timing off, summary off)
>   SELECT * FROM pred WHERE n BETWEEN 1000 AND 1002;
>                             QUERY PLAN
> −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
>  Index Scan using pred_n_idx on pred (actual rows=3 loops=1)
>    Index Cond: ((n >= '1000'::numeric) AND (n <= '1002'::numeric))
> (2 rows)
> ```

现在，不再是三个 tuple 类型的锁，而是一个 page 类型的锁。

```text
=> SELECT relation::regclass, locktype, page, tuple
FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 34753
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)

=> ROLLBACK;
```




页级锁的升级遵循相同的原则<span class="book-margin-note" aria-label="原书旁注">v. 10</span>。如果某个特定关系的页级锁的数量超过了 <em>max\_pred\_locks\_per\_relation</em><span class="book-margin-note" aria-label="原书旁注">-2</span> 值，他们将被一个单独的关系级锁替代。(如果此参数设置为负数，那么阈值为 <em>max\_pred\_locks\_per\_transaction</em><span class="book-margin-note" aria-label="原书旁注">64</span> 除以 <em>max\_pred\_locks\_per\_relation</em> 的绝对值；因此，默认的阈值为 32)。

锁升级肯定会导致多次 false-positive 序列化错误，这会对系统吞吐量产生负面影响。所以你必须在性能和在锁上花费的内存之间找到一个合适的平衡点。

谓词锁支持以下索引类型：

- B 树

- 哈希索引，GiST 和 GIN<span class="book-margin-note" aria-label="原书旁注">v. 11</span>

如果执行了索引扫描，但索引不支持谓词锁，那么整个索引将被锁定。可以预料，在这种情况下，无故中止的事务数量也会增加。

为了在可串行化级别下更高效地操作，使用 read only 子句显式声明只读事务是有意义的。如果锁管理器看到只读事务不会与其他事务发生冲突 [^zh-14-miscellaneous-locks-12]，它可以释放已经设置的谓词锁并避免获取新的谓词锁。如果这样的事务也被声明为 DEFERABLE 的，那么也可以避免只读事务异常。

[^zh-14-miscellaneous-locks-1]: backend/storage/lmgr/lmgr.c, LockDatabaseObject &amp; LockSharedObject functions

[^zh-14-miscellaneous-locks-2]: backend/catalog/dependency.c, performDeletion function

[^zh-14-miscellaneous-locks-3]: backend/storage/lmgr/lmgr.c, LockRelationForExtension function

[^zh-14-miscellaneous-locks-4]: backend/access/heap/hio.c, RelationAddExtraBlocks function

[^zh-14-miscellaneous-locks-5]: backend/access/nbtree/nbtpage.c, \_bt\_getbuf function

[^zh-14-miscellaneous-locks-6]: backend/storage/lmgr/lmgr.c, LockPage function

[^zh-14-miscellaneous-locks-7]: [postgresql.org/docs/14/explicit-locking\#ADVISORY-LOCKS.html](https://postgresql.org/docs/14/explicit-locking\#ADVISORY-LOCKS.html)

[^zh-14-miscellaneous-locks-8]: [postgresql.org/docs/14/functions-admin\#FUNCTIONS-ADVISORY-LOCKS.html](https://postgresql.org/docs/14/functions-admin\#FUNCTIONS-ADVISORY-LOCKS.html)

[^zh-14-miscellaneous-locks-9]: <em>K. P. Eswaran, J. N. Gray, R. A. Lorie, I. L. Traiger</em>. The notions of consistency and predicate locks in a database system

[^zh-14-miscellaneous-locks-10]: backend/storage/lmgr/README-SSI  
    backend/storage/lmgr/predicate.c

[^zh-14-miscellaneous-locks-11]: backend/storage/lmgr/predicate.c, PredicateLockAcquire function

[^zh-14-miscellaneous-locks-12]: backend/storage/lmgr/predicate.c, SxactIsROSafe macrou
