10.5 咨询锁与跨行协调
Advisory lock 让应用给一个整数 key 赋予“资源正在被协调”的含义。PostgreSQL lock manager 只知道 key、shared/exclusive、session/transaction lifetime;它不知道这个 key 是 tenant、invoice、cron job 还是部署。
因此 advisory lock 的正确性来自两部分:
PostgreSQL guarantees mutual exclusion for the same key
+
all participants voluntarily use the same key/lifetime/protocol任何绕过协议的 SQL 仍可修改底层 rows。
10.5.1 会话级与事务级咨询锁
两种 lifetime
transaction-level:
BEGIN;
SELECT pg_advisory_xact_lock(42);
-- protected database work
COMMIT; -- 自动释放,不能手工提前释放session-level:
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 泄漏风险 | 高 | 较低 |
本章真实验证:
pg_advisory_lock(3610,1015)
→ ROLLBACK
→ lock still granted
→ explicit unlock
pg_advisory_xact_lock(3610,1016)
→ COMMIT
→ lock count=0session lock 在 transaction rollback 后仍存在不是 bug。若 pool 把同一 backend 交给另一请求,它会继承 lock;重复 acquire 还会 stack。除非保护范围确实跨多个 transaction,并有严格 connection pin/unlock/finally,优先 xact lock。
blocking 与 try 版本
等待:
SELECT pg_advisory_xact_lock($key);
SELECT pg_advisory_xact_lock_shared($key);不等待:
SELECT pg_try_advisory_xact_lock($key); -- boolean
SELECT pg_try_advisory_xact_lock_shared($key); -- booleansession 级也有 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 偷锁
危险:
SELECT pg_advisory_lock(id)
FROM resource
WHERE id > 12345
LIMIT 100;SQL 不保证 volatile function 一定在 LIMIT 后只对最终 100 行求值;可能取得超出预期的 locks。先固定子查询:
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 键空间、碰撞与所有权
两种 key 形式互不重叠
PostgreSQL 提供:
one signed bigint
two signed integer values两个 key space 不重叠。团队应只选一种规范并写出 namespace:
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 能精确查:
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:
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,不记录:
业务 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 应设置:
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 不用咨询锁掩盖缺失的数据约束
能用 constraint 表达的仍交给 constraint
错误:
acquire advisory hash(email)
SELECT whether email exists
INSERT
release如果任一 importer、migration、另一个 service 忘记拿 lock,就可产生重复。正确 authority:
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 仍存表。
所有路径必须加入协议
若决定使用:
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。
若确实协调外部资源,至少有:
durable owner/lease row
fencing token
expiry/renewal
stale-owner recovery
idempotent remote operation
reconciliation仅有一个 session lock 无法提供 fencing:旧 owner 网络暂停后恢复,可能与新 owner 同时对外部系统操作。
审查问题
每个 advisory lock 提案必须回答:
- 为什么 row/constraint/Serializable 不能更直接表达?
- key space 与 collision policy 是什么?
- shared 还是 exclusive?
- session 还是 transaction lifetime,为什么?
- 谁是 owner,如何观察?
- 等待/try/timeout 语义是什么?
- error/rollback/pool release 时怎样保证释放?
- 所有写路径如何遵守?
- 进程崩溃后 durable state 在哪里?
- 是否跨外部系统,fencing/idempotency 如何做?
答不全就不是一个可上线协议。
延伸阅读
上一节:乐观控制、重试与幂等 · 返回本章目录 · 下一节:观察与诊断并发 · 查看全书目录 · 查看索引中心