跳至内容

5.4 锁与等待

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

5.4.1 表锁、行锁与轻量级锁的职责

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

层次保护对象/责任主要证据应用能否显式取得
table-level lockrelation 与 DDL/DML 的兼容性pg_locks,locktype=relationLOCK TABLE 或命令自动取得
row-level lock同一 tuple 的更新、删除、显式 locker 冲突tuple header、transaction-ID wait、部分 pg_locksSELECT ... FOR ... 或 DML
regular lock manager 其他对象XID、virtual XID、object、extend、advisory 等pg_locks部分可以
predicate lockSerializable read/write dependency 跟踪pg_locksSIReadLock由 SSI 自动管理,不阻塞
page/buffer pinbuffer 中页面访问的短期协调wait event / 内部状态不能作为业务锁 API
LWLockshared-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 同时存在

一次:

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 语义则是:

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 完成。

本章现场恰好展示:

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 的:

wait_event_type=Lock
wait_event=transactionid

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

5.4.2 等待图、阻塞链与死锁检测

把每个正在等锁的 backend 画成节点,waiter → blocker 画成有向边:

W2 ──waits for──> W1 ──waits for──> B0

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

T1 → T2 → T1

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

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

第一组只读证据:

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 后再补:

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.statewait_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 章会用确定性双事务场景验证 40P0140001 和重试边界。

5.4.3 锁模式名称不等于业务影响

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

命令示例自动取得的 relation mode对普通 SELECT
SELECTACCESS SHARE可并发
SELECT ... FOR UPDATE目标表 ROW SHARE,另有 row lock普通读仍可并发
INSERT/UPDATE/DELETE/MERGE目标表 ROW EXCLUSIVE普通读仍可并发
VACUUMANALYZECREATE INDEX CONCURRENTLY常见为 SHARE UPDATE EXCLUSIVE普通读可并发,但各命令还有阶段/资源代价
CREATE INDEX 非 concurrentlySHARE普通读可并发,写入受阻
TRUNCATEVACUUM FULL、许多 rewrite DDLACCESS EXCLUSIVE阻塞

官方矩阵中,普通 SELECTACCESS 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 贴“安全/危险”标签。

取消与终止是最后一步

生产处置顺序应是:

确认采样时刻
→ 定位 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 重用或误伤无关工作。


上一节:事务边界与失败语义 · 返回本章目录 · 下一节:隔离现象与后续路线 · 查看全书目录 · 查看索引中心

最后更新于