# 锁与等待

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

---

“数据库被锁了”通常把至少四件事混在一起：对象上的 regular lock、heap tuple 中的 row lock、共享内存内部的 lightweight lock，以及当前 backend 的 wait event。可靠诊断先确定等待类型，再建立谁等待谁的边，最后才评估是否需要取消或终止。

## 5.4.1 表锁、行锁与轻量级锁的职责 {#item-5-4-1}

PostgreSQL 用不同同步机制保护不同层次：

| 层次 | 保护对象/责任 | 主要证据 | 应用能否显式取得 |
|---|---|---|---|
| table-level lock | relation 与 DDL/DML 的兼容性 | `pg_locks`，locktype=`relation` | `LOCK TABLE` 或命令自动取得 |
| row-level lock | 同一 tuple 的更新、删除、显式 locker 冲突 | tuple header、transaction-ID wait、部分 `pg_locks` | `SELECT ... FOR ...` 或 DML |
| regular lock manager 其他对象 | XID、virtual XID、object、extend、advisory 等 | `pg_locks` | 部分可以 |
| predicate lock | Serializable read/write dependency 跟踪 | `pg_locks` 的 `SIReadLock` | 由 SSI 自动管理，不阻塞 |
| page/buffer pin | buffer 中页面访问的短期协调 | wait event / 内部状态 | 不能作为业务锁 API |
| LWLock | shared-memory data structure 的短期互斥 | `wait_event_type='LWLock'` | 不能 |
| advisory lock | 应用自定义的整数 key 协调 | `pg_locks` + advisory functions | 可以，但数据库不懂业务对象 |

“heavyweight lock”常被用来指 regular lock manager 中会入 lock table、支持等待队列和 deadlock detection 的对象；它不意味着一定很慢或锁住大范围。LWLock 的“lightweight”也不意味着可以忽略：高并发下某个共享结构的 LWLock contention 完全可能成为主要延迟，只是解决方式不是 `SELECT FOR UPDATE`。

### table lock 和 row lock 同时存在

一次：

```sql
UPDATE shop.sales_order
SET request_fingerprint = ...
WHERE order_id = 1002;
```

至少要保护：

- relation 上的 `ROW EXCLUSIVE` table-level lock，防止冲突 DDL；
- 目标 row version 的 row-level update lock；
- 当前 transaction ID 的状态与等待者；
- buffer/WAL 等内部结构的短期同步。

`ROW EXCLUSIVE` 名字中有 ROW，却是 table-level mode。row lock 的四种 SQL 语义则是：

```text
FOR KEY SHARE
FOR SHARE
FOR NO KEY UPDATE
FOR UPDATE
```

强度与冲突矩阵不同。普通 UPDATE 若不改变可用于 foreign key 的 key columns，通常取得较弱的 `FOR NO KEY UPDATE` 语义；修改 key 或 DELETE 会更强。应用不应根据一个通用单词“exclusive”推断所有冲突。

### 为什么 `pg_locks` 里看不到 blocker 的“行锁”

PostgreSQL 不把所有已锁行维护成一张无限增长的 shared-memory 清单；row lock 信息写在 tuple header。发生同一行 update conflict 时，waiter 常先取得一个 tuple lock 以排队，然后等待 blocker 的 transaction ID 完成。

本章现场恰好展示：

```text
blocker:
  transactionid | ExclusiveLock | granted=true | xid=962

waiter:
  tuple         | ExclusiveLock | granted=true  | sales_order page=0 tuple=4
  transactionid | ShareLock     | granted=false | xid=962
```

真正未获准的是 waiter 对 XID 962 的 `ShareLock`，所以 activity 的：

```text
wait_event_type=Lock
wait_event=transactionid
```

与 locks 证据一致。若只搜索 `locktype='tuple' AND granted=false`，会错误得出“没有行锁等待”。这也是为什么权威 blocker 边优先使用 `pg_blocking_pids(waiter_pid)`。

## 5.4.2 等待图、阻塞链与死锁检测 {#item-5-4-2}

把每个正在等锁的 backend 画成节点，`waiter → blocker` 画成有向边：

```text
W2 ──waits for──> W1 ──waits for──> B0
```

这是 blocking chain；只要 B0 最终 commit/rollback，链可以继续推进。若形成环：

```text
T1 → T2 → T1
```

才是 deadlock。等待很久不自动等于 deadlock，deadlock 也不要求等待很久才在逻辑上成立。

### 从 waiter 出发，而不是拼一条万能 self-join

第一组只读证据：

```sql
SELECT
    pid,
    application_name,
    state,
    wait_event_type,
    wait_event,
    xact_start,
    query_start,
    pg_blocking_pids(pid) AS blocking_pids,
    query
FROM pg_stat_activity
WHERE datname = current_database()
  AND state <> 'idle';
```

`pg_blocking_pids()`知道 lock conflict matrix、wait queue 和 parallel worker 映射，比手写 `pg_locks` self-join可靠。它既可能返回持有冲突锁的 hard blocker，也可能返回排在队列前面的 soft blocker；parallel query 可能出现重复 client-visible PID，prepared transaction blocker 用 PID 0 表示。高频调用还会短暂独占 lock manager shared state，所以它是诊断函数，不应被应用每毫秒轮询。

找到 edge 后再补：

```sql
SELECT *
FROM pg_locks
WHERE pid = ANY (ARRAY[waiter_pid, blocker_pid]);
```

用于解释对象、mode、granted、fastpath、waitstart。`pg_locks` 是瞬时切片；fast-path、regular 和 predicate lock 的采集并非一个全局冻结时刻，不要把两个相隔数秒的查询拼成绝对一致的历史。

### active 不等于正在消耗 CPU

`pg_stat_activity.state` 与 `wait_event` 独立：

- `state='active' AND wait_event IS NULL`：正在执行，但仍需结合 CPU/I/O 证据；
- `state='active' AND wait_event IS NOT NULL`：query 在执行生命周期中，却卡在某个 wait point；
- `idle in transaction`：当前没跑 query，但 transaction 仍开着，可能持锁和 snapshot；
- `idle`：等待客户端下一条命令，通常不持 transaction locks。

本章 waiter 是 `active + Lock + transactionid`，blocker 却是 `active + Timeout + PgSleep`。后者不是在等待 waiter，而是在按实验设计睡眠并持有未提交事务。只按 `state='active'` 排序会把二者都叫“活跃 SQL”，丢失因果关系。

### deadlock detector 解决环，不替应用设计顺序

PostgreSQL 检测到锁等待环后会 abort 其中一个 transaction，以 `40P01 deadlock_detected` 让其他成员继续；不能依赖固定谁当 victim。应用要 rollback 并从事务开头重试。

最有效的预防是所有代码按一致顺序取得多个对象的锁。例如转账总按较小 account ID 后较大 ID；批量更新先排序主键。还应：

- transaction 尽量短，不在持锁时等待用户或远程 API；
- 第一次取得对象时就选择实际需要的 mode，避免难以推理的升级；
- 为 lock wait 设置业务预算并保留原始 SQLSTATE；
- 在受控环境启用合适的 `log_lock_waits`/`deadlock_timeout` 取证；
- 监控连接池排队与 database lock 两种不同的“等待”。

本章不主动制造 deadlock，因为一次单边阻塞已经足以建立证据链；第 10 章会用确定性双事务场景验证 `40P01`、`40001` 和重试边界。

## 5.4.3 锁模式名称不等于业务影响 {#item-5-4-3}

table-level mode 的关键不是英文听感，而是 conflict matrix。常用子集如下：

| 命令示例 | 自动取得的 relation mode | 对普通 `SELECT` |
|---|---|---|
| `SELECT` | `ACCESS SHARE` | 可并发 |
| `SELECT ... FOR UPDATE` | 目标表 `ROW SHARE`，另有 row lock | 普通读仍可并发 |
| `INSERT/UPDATE/DELETE/MERGE` | 目标表 `ROW EXCLUSIVE` | 普通读仍可并发 |
| `VACUUM`、`ANALYZE`、`CREATE INDEX CONCURRENTLY` | 常见为 `SHARE UPDATE EXCLUSIVE` | 普通读可并发，但各命令还有阶段/资源代价 |
| `CREATE INDEX` 非 concurrently | `SHARE` | 普通读可并发，写入受阻 |
| `TRUNCATE`、`VACUUM FULL`、许多 rewrite DDL | `ACCESS EXCLUSIVE` | 阻塞 |

官方矩阵中，普通 `SELECT` 的 `ACCESS SHARE` 只与 `ACCESS EXCLUSIVE` 冲突。这不表示 DDL 只有 `ACCESS EXCLUSIVE` 才有业务影响：一个等待取得强锁的 DDL 可能排在队列中，让它后面的请求形成 convoy；CREATE INDEX 还可能争用 I/O/CPU；长 transaction 会让短暂 lock 变成长事故。

### 同一个 mode，影响可以相差几个数量级

评估锁风险至少要回答：

1. **对象**：哪张 relation、哪一行、哪个 XID 或 advisory key？
2. **mode 与 conflict**：谁与谁冲突，不是名字有多吓人？
3. **范围**：命中一行、百万行、所有 partition，还是 catalog object？
4. **持有期**：statement 结束还是 transaction 结束？事务已经多老？
5. **扇出**：有多少 waiter、上游连接池和同步请求？
6. **可恢复性**：cancel statement 足够，还是 backend 必须终止？commit outcome 是否 ambiguous？

对一行的 `ROW EXCLUSIVE` table lock 可以持续 2 ms，也可以因应用调用支付接口持续 30 s；mode 相同，业务影响完全不同。反之，一个瞬时 `ACCESS EXCLUSIVE` 若能立即取得并在毫秒内完成，可能比排队十分钟的普通写影响小。DDL 发布必须用真实锁时长、table size、long transaction 和 timeout 演练，而不是静态给 mode 贴“安全/危险”标签。

### 取消与终止是最后一步

生产处置顺序应是：

```text
确认采样时刻
→ 定位 waiter 与 blocker edge
→ 核对 application/user/database/xact age/query
→ 评估 blocker 是否正在做不可中断业务
→ 优先让 owner 正常结束
→ 必要时 pg_cancel_backend(query)
→ 明确授权后 pg_terminate_backend(session)
→ 验证锁链、业务状态与重试结果
```

`pg_cancel_backend` 只请求取消当前 query，不自动关闭 session。本章 blocker 之所以随后释放事务，是因为 psql 设置 `ON_ERROR_STOP`，收到 `57014 query_canceled` 后退出连接，server 因断连 rollback。生产 application 可能捕获错误后停在 failed/idle transaction；不能照抄实验把 cancel 当作 transaction cleanup。

`pg_terminate_backend` 会断开精确 session，影响更大；连接池还可能立刻重连并重放负载。任何处置都要保存 PID、backend start、application name、XID、query/transaction start 与 blocking edge，避免 PID 重用或误伤无关工作。

---

[上一节：事务边界与失败语义](../03/) · [返回本章目录](../) · [下一节：隔离现象与后续路线](../05/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
