# 关系级锁

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

---

<span id="internal-ru-chapter-12" class="pdf-anchor"></span><span id="internal-ru-page-256" class="pdf-anchor"></span>

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

## 12.1. 关于锁 {#internal-ru-section-12-1}

*锁*（locks），也称为 *锁*，用于协调对共享资源的并发访问。

并发访问指的是多个进程同时访问某个资源。这些进程可以并行执行（如果硬件允许），也可以在时间分片模式下顺序执行——这并不重要。如果没有竞争，就不需要锁（例如，共享缓冲区缓存需要锁，而本地缓存则不需要）。

在访问资源之前，进程必须 *获取*（acquire）与该资源关联的锁，而在完成工作后必须 *释放*（release）它，以便其他进程可以使用该资源。当锁由数据库管理系统（DBMS）管理时，这种顺序会自动维护；如果锁由应用程序设置，则必须由应用程序自身遵守规则。

在底层，锁表现为共享内存中的一段区域，其中以某种方式标记锁的状态（空闲或已被获取），并可能包含额外信息（如进程号和获取时间）。

> [!NOTE]
> 可以注意到，这段共享内存本身也是一个资源。对它的并发访问通过操作系统提供的特殊同步原语（如信号量或互斥锁）来协调。这些原语确保访问共享资源的代码严格按顺序执行。在最低层，这些原语

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

> [!NOTE]
> 是基于处理器的原子指令（如 test-and-set 或 compare-and-swap）实现的。

原则上，任何可以唯一标识并映射到锁地址的资源都可以作为受保护的资源。

例如，资源可以是数据库正在操作的对象，如表（通过系统目录中的 `oid` 标识）、数据页面（通过文件名和文件内位置标识）、行版本（通过页面和页面内偏移标识）。资源也可以是内存中的结构，如哈希表或缓冲区（通过预先分配的编号标识）。有时甚至方便使用没有实际物理意义的抽象资源。

锁的获取并非总是可行：资源可能已被其他进程占用。此时，进程要么进入队列（如果锁机制支持），要么在一段时间后重试获取。无论如何，这都会导致进程必须等待资源释放。

我将强调两个显著影响锁效率的因素。

**粒度**——锁的细化程度。当资源具有层次结构时，粒度尤为重要。

例如，表由页组成，页包含行版本。这些对象都可以作为资源。如果在表级别设置锁（粗粒度），即使多个进程访问不同的页或行，也无法同时工作。

行级锁（细粒度）没有这一缺点，但锁的数量会显著增加。为了防止这些信息占用过多内存，可以采用各种方法，例如 *升级*（escalation）：当低级别、细粒度锁的数量超过某个阈值时，它们会被一个更高级别的锁所替代。

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

**锁模式集合**，即锁可以被获取的模式。

通常只使用两种模式。*排他*（exclusive）或 *独占* 模式与任何其他模式（包括自身）都不兼容。*共享*（shared）模式允许多个进程同时获取锁。共享模式通常用于读取资源，而排他模式用于修改资源。

一般情况下，模式可以更多。模式名称本身并不重要，重要的是它们之间的兼容性矩阵。

粒度越细、可兼容的模式越多，就越能实现并行化。

锁可以根据使用时间进行分类。

**长期锁**可能持有很长时间（通常直到事务结束），通常用于关系和行等资源。数据库系统通常自动管理这些锁，但用户仍可在某种程度上控制该过程。

长期锁具有较多模式，允许对数据执行多种并发操作。通常这类锁配有完善的基础设施（等待队列、死锁检测、监控），因为维护这些机制的成本远低于保护数据的操作成本。

**短期锁**持有时间仅为几毫秒（通常仅为执行几条处理器指令的时间），通常用于共享内存中的数据结构。数据库系统完全自动管理这些锁。

短期锁的特点是模式最少，基础设施简单，甚至可能没有监控机制。

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

PostgreSQL 使用多种类型的锁[^internal-ru-258-1]。长期锁包括 *重锁*（在关系及其他对象级别）；单独列出 *行级锁*。短期锁包括各种 *内存结构上的锁*。特别的是 *谓词锁*，尽管名称如此，但它们并非真正的锁。（参见 [268](/zh18/13-row-level-locks/#internal-ru-page-269)）（参见 [305](/zh18/15-locks-on-memory-structures/#internal-ru-page-306)）（参见 [298](/zh18/14-miscellaneous-locks/#internal-ru-page-299)）

## 12.2. 重锁 {#internal-ru-section-12-2}

*重锁*（heavyweight）属于长期锁。它们在 *对象* 级别设置：主要是关系，但也包括其他一些对象。这些锁通常用于防止对象被同时修改或在重组期间被使用，但也可能用于其他用途。这种模糊的定义源于这些锁被用于各种不同目的，而它们的共同点仅在于其结构方式。

通常，当未明确说明时，“锁”指的就是这种重锁。

重锁位于服务器的共享内存中 [^internal-ru-259-1]，可通过 `pg_locks` 视图查看。其数量受限于两个服务器参数值的乘积：*max\_locks\_per\_transaction* 和 *max\_connections*。（默认值：`64`）（默认值：`100`）

锁池对所有事务共享，即一个事务可以获取超过 *max\_locks\_per\_transaction* 数量的锁。重要的是系统中锁的总数不超过设定上限。锁池在服务器启动时创建，因此修改任一参数都需要重启服务器。

如果资源已被不兼容模式的锁占用，试图获取锁的进程将被放入队列。等待的进程不消耗 CPU 时间：它们休眠，并在资源释放时由操作系统唤醒。

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

可能发生 *死锁*（deadlock），即两个事务陷入 *僵局*，其中第一个事务需要第二个事务占用的资源，而第二个事务又需要第一个事务占用的资源。这是最简单的情况；多个事务也可能陷入僵局。由于死锁会导致无限等待，PostgreSQL 会自动检测僵局并强制中止其中一个事务，以使其他事务得以继续执行。（参见 [286](/zh18/13-row-level-locks/#internal-ru-page-287)）

由于不同类型的重锁服务于不同目的，保护不同的资源，并具有不同的模式，因此每种类型都需单独讨论。

以下列表中的名称对应 `pg_locks` 视图中 `locktype` 列的值：

**transactionid** 和 **virtualxid** —— 事务编号锁；（参见 [259](/zh18/12-relation-level-locks/#internal-ru-page-260)）

**relation** —— 关系锁；（参见 [261](/zh18/12-relation-level-locks/#internal-ru-page-262)）

**tuple** —— 行版本锁；（参见 [275](/zh18/13-row-level-locks/#internal-ru-page-276)）

**object** —— 非关系对象的锁；（参见 [293](/zh18/14-miscellaneous-locks/#internal-ru-page-294)）

**extend** —— 在向关系文件添加新页面时的文件锁；（参见 [295](/zh18/14-miscellaneous-locks/#internal-ru-page-296)）

**page** —— 页面锁（某些索引类型使用）；（参见 [296](/zh18/14-miscellaneous-locks/#internal-ru-page-297)）

**advisory** —— 建议性锁。（参见 [296](/zh18/14-miscellaneous-locks/#internal-ru-page-297)）

几乎所有重锁都是在需要时自动设置，并在事务结束时自动释放。例外情况包括：关系锁可以显式请求，而建议性锁的管理完全由用户控制。

## 12.3. 事务 ID 上的锁 {#internal-ru-section-12-3}

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

每个事务始终持有其自身编号（虚拟和真实，若存在）的排他锁。（参见 [91](/zh18/03-pages-and-tuples/#internal-ru-page-92)）

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

使用两种锁模式：排他和共享。冲突矩阵非常简单：共享模式与自身兼容；排他模式与任何模式都不兼容。

| 模式 | Shared | Exclusive |
| --- | --- | --- |
| Shared |  | × |
| Exclusive | × | × |
{#internal-ru-table-12-1 num="12.1" caption="锁模式冲突矩阵：Shared 与 Exclusive" class="wide"}

要等待某个事务结束，只需请求其编号的锁（任意模式）。由于该事务本身已持有其编号的排他锁，因此无法获取。请求进程将进入队列并休眠。当事务结束时，锁被释放，等待的进程将被唤醒。当然，它无法获取所请求的锁——资源已不存在。但这并非必需。

在独立会话中启动一个事务并获取其服务进程编号：

```text {label="psql"}
=> BEGIN;
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
          64412
(1 row)
```

刚刚启动的事务持有其自身虚拟编号的排他锁：

```text {label="psql"}
=> SELECT locktype, virtualxid, mode, granted
FROM pg_locks WHERE pid = 64412;
  locktype  | virtualxid |     mode      | granted
------------+------------+---------------+---------
 virtualxid | 4/2        | ExclusiveLock | t
(1 row)
```

此处 `locktype` 为锁类型，`virtualxid` 为事务的虚拟编号（标识资源），`mode` 为模式（此处为排他）。`granted` 标志显示是否成功获取了请求的锁。

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

当事务获得真实编号时，其锁也会被添加到列表中：

```text {label="psql"}
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
             191442
(1 row)
```

```text {label="psql"}
=> SELECT locktype, virtualxid, transactionid AS xid, mode, granted
FROM pg_locks WHERE pid = 64412;
   locktype    | virtualxid |  xid   |     mode      | granted
---------------+------------+--------+---------------+---------
 virtualxid    | 4/2        |        | ExclusiveLock | t
 transactionid |            | 191442 | ExclusiveLock | t
(2 rows)
```

现在事务持有其两个编号的排他锁。

## 12.4. 关系级锁 {#internal-ru-section-12-4}

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

关系锁定义了八种不同的模式[^internal-ru-262-1]。如此多的模式是为了尽可能让针对同一关系（表、索引或其他对象）的多种命令能够并发执行。

下图展示了冲突矩阵，并附有需要相应锁模式的命令示例。没有必要试图记忆这些模式或在它们的名称中寻找逻辑。但仔细研究这张表，得出一般性结论，并在需要时查阅——毫无疑问，这是值得的。

| 模式 | AS | RS | RE | SUE | S | SRE | E | AE | 命令 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Access Share |  |  |  |  |  |  |  | × | SELECT |
| Row Share |  |  |  |  |  |  | × | × | SELECT FOR UPDATE/SHARE |
| Row Exclusive |  |  |  |  | × | × | × | × | INSERT, UPDATE, DELETE |
| Share Update Exclusive |  |  |  | × | × | × | × | × | VACUUM, CREATE INDEX CONCURRENTLY |
| Share |  |  | × | × |  | × | × | × | CREATE INDEX |
| Share Row Exclusive |  |  | × | × | × | × | × | × | CREATE TRIGGER |
| Exclusive |  | × | × | × | × | × | × | × | REFRESH MAT. VIEW CONCURRENTLY |
| Access Exclusive | × | × | × | × | × | × | × | × | DROP, TRUNCATE, VACUUM FULL, LOCK TABLE, REFRESH MAT. VIEW |
{#internal-ru-table-12-2 num="12.2" caption="关系锁冲突矩阵" class="wide"}

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

第一个模式 Access Share 是最弱的。它与除最后一个模式 Access Exclusive 外的任何其他模式都兼容。因此，`SELECT` 命令几乎不会妨碍其他命令的执行，但例如，它不会允许删除正在被查询访问的表。相反，Access Exclusive 模式是最严格的：它与任何其他模式都不兼容，即具有排他性。

前四种模式允许同时对表中的数据进行修改，而后四种模式则不允许。例如，`CREATE INDEX` 命令使用 Share 模式。该模式与自身兼容（即可以同时为同一张表创建多个索引），也与读取类命令的模式兼容。因此，`SELECT` 命令可以执行，但 `INSERT`、`UPDATE` 和 `DELETE` 命令将被阻塞。

相反，正在修改表中数据的未完成事务将阻塞 `CREATE INDEX` 命令的执行。因此，存在 `CREATE INDEX CONCURRENTLY` 选项，它使用较弱的 Share Update Exclusive 模式：该命令创建索引的时间比普通命令更长（甚至可能失败），但允许同时进行数据修改。

`ALTER TABLE` 命令有多种变体，需要不同的锁模式（Share Update Exclusive、Share Row Exclusive、Access Exclusive）。当然，所有模式都在文档中都有描述[^internal-ru-263-1]。

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

本章中的示例将使用我们已熟悉的账户表：

```text {label="psql"}
=> TRUNCATE accounts;
=> INSERT INTO accounts(id, client, amount)
VALUES (1, 'alice',   100.00),
       (2, 'bob',     200.00),
       (3, 'charlie', 300.00);
```

查询锁表的操作还会多次用到，因此我们为它创建一个视图，将所有标识符合并为一个简短输出的通用列：

```text {label="psql"}
=> CREATE VIEW locks AS
SELECT pid,
  locktype,
  CASE locktype
    WHEN 'relation' THEN relation::regclass::text
    WHEN 'transactionid' THEN transactionid::text
    WHEN 'virtualxid' THEN virtualxid
  END AS lockid,
  mode,
  granted
FROM pg_locks
ORDER BY 1, 2, 3;
```

第一个会话中的事务（尚未结束）正在更新一行。与表一起，该表的所有索引也被锁定，因此出现了两个类型为 `relation`、模式为 Row Exclusive 的新锁：

```text {label="psql"}
=> UPDATE accounts SET amount = amount + 100.00 WHERE id = 1;
```

```text {label="psql"}
=> SELECT locktype, lockid, mode, granted
FROM locks WHERE pid = 64412;
   locktype    |    lockid     |       mode       | granted
---------------+---------------+------------------+---------
 relation      | accounts      | RowExclusiveLock | t
 relation      | accounts_pkey | RowExclusiveLock | t
 transactionid | 191442        | ExclusiveLock    | t
 virtualxid    | 4/2           | ExclusiveLock    | t
(4 rows)
```

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

## 12.5. 等待队列 {#internal-ru-section-12-5}

重锁提供“公平”的等待队列[^internal-ru-265-1]。如果一个进程试图获取与已持有锁的模式不兼容，或与队列中已有等待进程的模式不兼容的锁，它就会进入等待队列。

当第一个会话中的事务正在执行更新时，尝试在另一个会话中为该表创建索引：

```text {label="psql"}
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
          64897
(1 row)
=> CREATE INDEX ON accounts(client);
```

该命令“挂起”以等待资源释放。事务试图以 Share 模式获取表锁，但无法成功：

```text {label="psql" hl_lines="5"}
=> SELECT locktype, lockid, mode, granted
FROM locks WHERE pid = 64897;
  locktype  |  lockid  |     mode      | granted
------------+----------+---------------+---------
 relation   | accounts | ShareLock     | f
 virtualxid | 6/3      | ExclusiveLock | t
(2 rows)
```

现在，假设在第三个会话中启动了 `VACUUM FULL` 命令。它也会进入队列，因为其所需的 Access Exclusive 模式与任何其他模式都不兼容：

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

```text {label="psql" hl_lines="11"}
      => SELECT pg_backend_pid();
       pg_backend_pid
      ----------------
                65106
      (1 row)
      => VACUUM FULL accounts;
=> SELECT locktype, lockid, mode, granted
FROM locks WHERE pid = 65106;
   locktype    |  lockid  |        mode         | granted
---------------+----------+---------------------+---------
 relation      | accounts | AccessExclusiveLock | f
 transactionid | 191446   | ExclusiveLock       | t
 virtualxid    | 8/4      | ExclusiveLock       | t
(3 rows)
```

现在，所有后续请求者都将进入队列，无论其锁模式如何。即使普通的 `SELECT` 查询也会诚实地排在 `VACUUM FULL` 之后，尽管它们与第一个会话中执行 `UPDATE` 命令所持有的 Row Exclusive 锁是兼容的。

```text {label="psql"}
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
          65322
(1 row)
=> SELECT * FROM accounts;
```

```text {label="psql" hl_lines="5"}
=> SELECT locktype, lockid, mode, granted
FROM locks WHERE pid = 65322;
  locktype  |  lockid  |      mode       | granted
------------+----------+-----------------+---------
 relation   | accounts | AccessShareLock | f
 virtualxid | 10/3     | ExclusiveLock   | t
(2 rows)
```

![等待队列：T1；UPDATE；关系；T2；CREATE INDEX；T3；VACUUM FULL；T4；SELECT](/images/internal/ru/p266-01.svg)
{#internal-ru-fig-12-1 num="12.1" caption="等待队列" .book-original-figure width=552 height=290}

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

函数 `pg_blocking_pids` 可以提供等待情况的总体视图。它显示了在指定进程之前排队的进程编号，这些进程要么持有不兼容的锁，要么正在请求不兼容的锁：

```text {label="psql"}
=> SELECT pid,
  pg_blocking_pids(pid),
  wait_event_type,
  state,
  left(query,50) AS query
FROM pg_stat_activity
WHERE pid IN (64412,64897,65106,65322) \gx
-[ RECORD 1 ]----+---------------------------------------------------
pid              | 64412
pg_blocking_pids | {}
wait_event_type  | Client
state            | idle in transaction
query            | UPDATE accounts SET amount = amount + 100.00 WHERE
-[ RECORD 2 ]----+---------------------------------------------------
pid              | 64897
pg_blocking_pids | {64412}
wait_event_type  | Lock
state            | active
query            | CREATE INDEX ON accounts(client);
-[ RECORD 3 ]----+---------------------------------------------------
pid              | 65106
pg_blocking_pids | {64412,64897}
wait_event_type  | Lock
state            | active
query            | VACUUM FULL accounts;
-[ RECORD 4 ]----+---------------------------------------------------
pid              | 65322
pg_blocking_pids | {65106}
wait_event_type  | Lock
state            | active
query            | SELECT * FROM accounts;
```

更详细的视图可以通过仔细分析 `pg_locks` 表的内容获得[^internal-ru-267-1]。

当事务结束（无论通过提交还是回滚）时，其所有锁都会被释放[^internal-ru-267-2]。队列中的第一个进程将获得所需的锁并被唤醒。

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

> [!NOTE]
> 一个不明显的情况是，当回滚到保存点（通过 `ROLLBACK TO SAVEPOINT` 命令或进入 PL/pgSQL 块的 `EXCEPTION` 部分）时，事务自该保存点以来获取的所有锁都会被释放。这是由于嵌套事务的回滚及其锁的释放所致。

在我们的示例中，第一个会话中事务的结束将导致队列中所有查询的依次执行：（参见 [92](/zh18/03-pages-and-tuples/#internal-ru-page-93)）

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

```sql
CREATE INDEX
```

```sql
VACUUM
```

```text
 id | client  | amount
----+---------+--------
  1 | alice   | 100.00
  2 | bob     | 200.00
  3 | charlie | 300.00
(3 rows)
```

[^internal-ru-258-1]: [backend/storage/lmgr/README](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/README;hb=REL_18_STABLE>).
[^internal-ru-259-1]: [backend/storage/lmgr/lock.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/lock.c;hb=REL_18_STABLE>).
[^internal-ru-262-1]: [postgrespro.ru/docs/postgresql/18/explicit-locking#LOCKING-TABLES](<https://postgrespro.ru/docs/postgresql/18/explicit-locking#LOCKING-TABLES>).
[^internal-ru-263-1]: [postgrespro.ru/docs/postgresql/18/sql-altertable](<https://postgrespro.ru/docs/postgresql/18/sql-altertable>).
[^internal-ru-265-1]: [backend/storage/lmgr/lock.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/lock.c;hb=REL_18_STABLE>), 函数 LockAcquire.
[^internal-ru-267-1]: [wiki.postgresql.org/wiki/Lock\_dependency\_information](<https://wiki.postgresql.org/wiki/Lock_dependency_information>).
[^internal-ru-267-2]: [backend/storage/lmgr/lock.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/lock.c;hb=REL_18_STABLE>), 函数 LockReleaseAll 和 LockRelease.
