# 咨询锁与跨行协调

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

---

Advisory lock 让应用给一个整数 key 赋予“资源正在被协调”的含义。PostgreSQL lock manager 只知道 key、shared/exclusive、session/transaction lifetime；它不知道这个 key 是 tenant、invoice、cron job 还是部署。

因此 advisory lock 的正确性来自两部分：

```text
PostgreSQL guarantees mutual exclusion for the same key
+
all participants voluntarily use the same key/lifetime/protocol
```

任何绕过协议的 SQL 仍可修改底层 rows。

## 10.5.1 会话级与事务级咨询锁 {#item-10-5-1}

### 两种 lifetime

transaction-level：

```sql
BEGIN;
SELECT pg_advisory_xact_lock(42);
-- protected database work
COMMIT;  -- 自动释放，不能手工提前释放
```

session-level：

```sql
SELECT pg_advisory_lock(42);
-- protected session work
SELECT pg_advisory_unlock(42);
```

关键差异：

| 行为 | session-level | transaction-level |
|---|---|---|
| transaction commit/rollback | 继续持有 | 自动释放 |
| 手工 unlock | 支持 | 不支持 |
| 同 session 重复 acquire | 计数叠加，需同次数 unlock | 同事务内不产生额外释放责任 |
| connection 结束 | 全部释放 | 当前事务结束时释放 |
| pool 泄漏风险 | 高 | 较低 |

本章真实验证：

```text
pg_advisory_lock(3610,1015)
  → ROLLBACK
  → lock still granted
  → explicit unlock

pg_advisory_xact_lock(3610,1016)
  → COMMIT
  → lock count=0
```

session lock 在 transaction rollback 后仍存在不是 bug。若 pool 把同一 backend 交给另一请求，它会继承 lock；重复 acquire 还会 stack。除非保护范围确实跨多个 transaction，并有严格 connection pin/unlock/finally，优先 xact lock。

### blocking 与 try 版本

等待：

```sql
SELECT pg_advisory_xact_lock($key);
SELECT pg_advisory_xact_lock_shared($key);
```

不等待：

```sql
SELECT pg_try_advisory_xact_lock($key);         -- boolean
SELECT pg_try_advisory_xact_lock_shared($key);  -- boolean
```

session 级也有 `pg_advisory_lock*`/`pg_try_advisory_lock*` 与显式 unlock。shared locks 彼此兼容，但与 exclusive 冲突。

选择与 row lock 类似：

- 必须串行且允许等待 → blocking + timeout；
- leader/cron 若已有 owner 就跳过 → try；
- 多 reader、单 writer → shared/exclusive，但协议更难；
- transaction 内数据库不变量 → xact lock；
- 跨事务外部资源 → session lock 只在有明确租约/断连语义时使用。

Advisory lock 也会参与 deadlock detection。两个 transaction 反向取得 advisory keys，同样可能 40P01。

### 不要让 SQL expression order 偷锁

危险：

```sql
SELECT pg_advisory_lock(id)
FROM resource
WHERE id > 12345
LIMIT 100;
```

SQL 不保证 volatile function 一定在 `LIMIT` 后只对最终 100 行求值；可能取得超出预期的 locks。先固定子查询：

```sql
SELECT pg_advisory_lock(resource.id)
FROM (
    SELECT id
    FROM resource
    WHERE id > 12345
    ORDER BY id
    LIMIT 100
) AS resource;
```

即使如此，一次取得 100 个 session locks 也需要完整 unlock/failure 设计。更常见的安全选择是逐批 transaction-level lock 或重新设计 queue。

## 10.5.2 键空间、碰撞与所有权 {#item-10-5-2}

### 两种 key 形式互不重叠

PostgreSQL 提供：

```text
one signed bigint
two signed integer values
```

两个 key space 不重叠。团队应只选一种规范并写出 namespace：

```text
key1 = domain namespace
key2 = stable resource identifier

(100, tenant_id)   tenant maintenance
(200, report_id)   report generation
(300, shard_id)    shard rebalance
```

本章保留 `(3610,1001..1016)`，所以 fixture cleanup 能精确查：

```sql
SELECT *
FROM pg_locks
WHERE locktype = 'advisory'
  AND classid = 3610::oid
  AND objid BETWEEN 1001::oid AND 1016::oid;
```

`pg_locks` 的 `classid/objid/objsubid` 是 lock manager 编码，不是自带业务字典。runbook 必须能把数字反解为 domain/resource，并保留 application name。

### hash 不是无碰撞 identity

把任意字符串压成 64-bit：

```text
hash(tenant || ':' || external_id)
```

理论上会碰撞。低概率碰撞对“偶尔多串行一次”可能可接受，对“错误资源被授权/跳过”则不可接受。评审：

- 输入 canonicalization；
- tenant/domain 是否进入 key；
- hash algorithm/seed 是否跨语言稳定；
- collision 的错误方向；
- 是否能直接使用无碰撞的 numeric ID；
- mapping 版本升级如何兼容；
- 同一资源的所有 caller 是否实现一致。

不要使用语言运行时每进程随机化的 `hash()`；不同 process 可能为同一字符串产生不同 key，互斥完全失效。

### lock 没有内建 owner metadata

PostgreSQL 记录 backend PID/session、lock mode、key 与 granted，不记录：

```text
业务 owner
request id
lease expiry
why acquired
runbook
```

这些要来自：

- unique `application_name`；
- request/job identity；
- transaction/session start；
- companion owner table（若需要 durable lease）；
- structured log/trace；
- documented key dictionary。

session 断开会释放 lock，因此 advisory lock 不是 durable ownership record。需要“进程死后仍知道谁做到哪一步”的任务系统，应把 state/lease/checkpoint 存表，lock 只协调瞬时竞争。

Advisory locks 使用 shared lock memory，受 `max_locks_per_transaction` 与连接数规模影响；大量不同 keys 不是免费 distributed cache。不要为每个长期对象永久持锁。

### timeout 与 cancellation

blocking advisory function obeys session/statement timeout 与 cancellation。生产 transaction family 应设置：

```sql
SET LOCAL lock_timeout = '200ms';
SET LOCAL statement_timeout = '2s';
```

具体预算由 SLO 决定。timeout 后 transaction 进入 failed state，需要 rollback；不能在同一 transaction 当作“没拿到锁，继续无锁执行”。若产品语义是立即跳过，用 `pg_try_advisory_xact_lock()` 的 boolean 更清晰。

## 10.5.3 不用咨询锁掩盖缺失的数据约束 {#item-10-5-3}

### 能用 constraint 表达的仍交给 constraint

错误：

```text
acquire advisory hash(email)
SELECT whether email exists
INSERT
release
```

如果任一 importer、migration、另一个 service 忘记拿 lock，就可产生重复。正确 authority：

```sql
UNIQUE (normalized_email)
```

advisory lock 可以减少预期冲突噪声，却不能替代 unique/exclusion/FK/check。

类似：

| 不变量 | 首选 |
|---|---|
| 单列/组合唯一 | `UNIQUE` |
| 引用存在 | `FOREIGN KEY` |
| 同房间时间不重叠 | `EXCLUDE` |
| 单行值域 | `CHECK` |
| version 未变化 | conditional UPDATE |
| queue item 只被一 worker claim | row lock + state transition |
| 跨行 predicate 可串行化 | Serializable/guard row/模型 |

Advisory lock 适合数据库没有天然 row 可锁、且不变量难以直接约束的短协调，例如：

- 每 tenant 同时只运行一个 schema backfill；
- 同一报表参数只生成一次昂贵结果；
- cron leader 竞争；
- 按 external resource ID 协调，但 durable state 仍存表。

### 所有路径必须加入协议

若决定使用：

```text
API writer
batch/import
admin script
retry worker
migration
repair/reconciliation
```

都必须调用同一个 key mapping 和 lifetime wrapper。通过受控数据库 function 封装可降低漂移，但仍要：

- schema-qualify；
- 固定 search_path；
- 处理权限；
- 返回 lock outcome；
- 约束 timeout；
- 写测试证明竞争与释放；
- 保留 constraint 作为可表达不变量的最后 authority。

### 不在锁内等待外部系统

“先拿 advisory lock，再调用 payment provider”仍会把外部延迟塞进 PostgreSQL lock queue；session 中断还会释放 lock，而 provider 可能已处理。支付应使用 durable idempotency state/outbox/reconciliation，不把 advisory lock 当跨系统 distributed transaction。

若确实协调外部资源，至少有：

```text
durable owner/lease row
fencing token
expiry/renewal
stale-owner recovery
idempotent remote operation
reconciliation
```

仅有一个 session lock 无法提供 fencing：旧 owner 网络暂停后恢复，可能与新 owner 同时对外部系统操作。

### 审查问题

每个 advisory lock 提案必须回答：

1. 为什么 row/constraint/Serializable 不能更直接表达？
2. key space 与 collision policy 是什么？
3. shared 还是 exclusive？
4. session 还是 transaction lifetime，为什么？
5. 谁是 owner，如何观察？
6. 等待/try/timeout 语义是什么？
7. error/rollback/pool release 时怎样保证释放？
8. 所有写路径如何遵守？
9. 进程崩溃后 durable state 在哪里？
10. 是否跨外部系统，fencing/idempotency 如何做？

答不全就不是一个可上线协议。

## 延伸阅读

- [PostgreSQL 18：Advisory Locks](https://www.postgresql.org/docs/18/explicit-locking.html#ADVISORY-LOCKS)
- [PostgreSQL 18：Advisory Lock Functions](https://www.postgresql.org/docs/18/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS)
- [PostgreSQL 18：`pg_locks`](https://www.postgresql.org/docs/18/view-pg-locks.html)

---

[上一节：乐观控制、重试与幂等](../04/) · [返回本章目录](../) · [下一节：观察与诊断并发](../06/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
