跳至内容

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-leveltransaction-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=0

session 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);  -- 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 偷锁

危险:

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_locksclassid/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 claimrow 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 提案必须回答:

  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 如何做?

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

延伸阅读


上一节:乐观控制、重试与幂等 · 返回本章目录 · 下一节:观察与诊断并发 · 查看全书目录 · 查看索引中心

最后更新于