编程 pg_locks 一行不等于一个锁:自连接查阻塞链的四个失效点

2026-11-15 00:04:22

pg_locks 一行不等于一个锁:自连接查阻塞链的四个失效点

SELECT * FROM pg_locks 的行数经常和「现在有多少个锁」对不上。同一个表上几个 backend 各持一把 AccessShareLock,就是几行;一个事务等另一个事务,等待者一行 granted=false,被等的行锁却不在视图里;fastpath=true 的行在采集过程中可能已经释放。拿 pg_locks 自连接展开「谁挡谁」,会在四个位置给出错误答案。

文档:pg_locks;Lock Monitoring wiki。以下结论按 PostgreSQL 官方文档语义整理,未做压测。

需要阻塞链时用 pg_blocking_pids(pid),不要手写 self join。

现象

pg_locks 为每个「可锁对象 + 请求的锁模式 + 相关进程」组合保留一行,所以同一个可锁对象会出现多行,当前没有任何锁的对象不会出现。granted=true 表示持有,false 表示等待;等待中的进程同一时间最多等一个锁,因此 WHERE NOT granted 的行数就是等待请求数。

问题出在「行」和「锁」不是一一对应:

  • 一个事务等另一个事务结束,等待者那行是 locktype='transactionid',但目标行锁不在 pg_locks 里;
  • fastpath=true 的行逐个 backend 采集,和主锁表不是同一个快照;
  • pg_locks 是集群级视图,relation 列 join pg_class 只在当前库成立;
  • prepared transaction 持有锁时 pid 为 null,按 pid 匹配会漏掉它。

判据与排查顺序

1. 先看 granted,不要先看行数

granted 为 true 是持有,false 是正在等待。不要用 waitstart IS NULL 判断「没在等」:已持有时 waitstart 为 null,但刚开始等待的极短时间内,即使 granted=false,这一列也可能还是 null。判断等待只看 granted。

pganalyze 5mins E25 给的口径也是这个:锁总数受大量 AccessShareLock 影响,不值得看,未授予锁的数量才有意义。

查未授予锁并带表名,跨库安全:

SELECT
l.pid,
l.locktype,
l.mode,
l.waitstart,
l.database,
CASE
WHEN l.database = (SELECT oid FROM pg_database WHERE datname = current_database())
OR l.database = 0
THEN l.relation::regclass::text
END AS relation_name
FROM pg_locks l
WHERE NOT l.granted
ORDER BY l.waitstart NULLS LAST;

relation::regclass 只在当前库或共享对象(database=0)下有意义,原因见第 5 条。

2. 用身份列的 null 语义判断锁对象

locktype 取值:relation、extend、frozenid、page、tuple、transactionid、virtualxid、spectoken、object、userlock、advisory、applytransaction。

各身份列什么时候为 null:

  • database:锁目标所在库 OID;共享对象为 0;事务 ID 类锁为 null。
  • relation:目标不是 relation 或 relation 的一部分时为 null。page、tuple 是 relation 的一部分,这两类行通常带 relation。
  • page:null 表示不是 relation page 或 tuple。
  • tuple:null 表示不是 tuple。
  • transactionid:只在 locktype='transactionid' 时有值;虚拟事务 ID 在 virtualtransaction 列。
  • classid / objid / objsubid:通用数据库对象才有值;objsubid 是列号,0 表示其它通用对象。
  • pid:prepared transaction 持有锁时为 null。
  • mode:锁模式名。
  • fastpath:true 走 fast path,false 走主锁表。

看到 locktype='transactionid' 且 granted=false,等待者是在等一个事务结束,不是等某一行。

3. 行锁不在 pg_locks

行级锁信息存在磁盘上,不放在共享内存的锁表里,因此不会出现在 pg_locks。一个 backend 等行锁时,表现为等待持有该行锁的事务的永久事务 ID:locktype='transactionid'、granted=false。DBAtlas 的配方把这种情况叫行级冲突,等待者会一直等到修改该行的事务结束。

不要在 pg_locks 里找 tuple 行定位行锁等待。看到 transactionid 等待,去 pg_stat_activity.backend_xid / backend_xmin 和业务 SQL 里找是哪个事务还没提交。

4. fastpath 行不能用来判断阻塞

pg_locks 同时暴露常规锁管理器和谓词锁管理器,常规锁管理器又分 regular 和 fast-path 两套,数据不保证一致。

查询视图时,fastpath=true 的行是逐个 backend 采集的,采集时没有冻结整个锁管理器,采集过程中锁可能被获取或释放。所有 backend 的 fast-path 锁采集完之后,常规锁管理器的剩余部分才整体加锁、一次性收集一致快照;之后以同样方式采集谓词锁管理器。因此除 fast-path 行外,每个锁管理器给出一致结果集,但两个管理器之间仍可能有时间差。

关键点:fast-path 锁已知与当前任何其它锁不冲突。自连接模板如果只按身份列匹配、不编码冲突矩阵,fastpath 行参与匹配就会造出不存在的 blocker 边;同一次查询里,fastpath 行和主锁表快照也可能自相矛盾。

数一下 fastpath 行有没有把统计撑大:

SELECT
count(*) AS total_rows,
count(*) FILTER (WHERE fastpath) AS fastpath_rows,
count(*) FILTER (WHERE NOT granted) AS waiting_rows
FROM pg_locks;

total_rows 里通常大部分是 AccessShareLock 和 fastpath 行,这个数字本身没有诊断价值;waiting_rows 才是要盯的。

5. 跨库 join pg_class 只在当前库成立

pg_locks 是集群级视图,返回所有数据库的锁,不只是当前库。relation 列可以 join pg_class.oid 识别被锁的表,但只对当前数据库中的 relation(database 为当前库 OID 或 0 的行)正确。

跨库时 OID 可能相同:库 A 的 public.orders 和库 B 的 public.orders 在不同 pg_class 里可能有同一个 OID,直接 join 会把别的库的表名贴到当前库的锁上。过滤方式二选一:

-- 只保留当前库和共享对象
WHERE l.database = (SELECT oid FROM pg_database WHERE datname = current_database())
OR l.database = 0

-- 或者显式限定库名
WHERE l.database IN (SELECT oid FROM pg_database WHERE datname IN ('app', 'app_shard_1'))

6. prepared transaction 的 pid 是 null

prepared transaction 永远不会在等待锁,但它会继续持有运行期间获得的锁。它在 pg_locks 里的 pid 为 null,virtualtransaction 形如 -1/。自连接按 pid 匹配时,它作为 blocker 列不出来。

关联回 pg_prepared_xacts:

SELECT
l.pid,
l.locktype,
l.mode,
l.database,
CASE
WHEN l.database = (SELECT oid FROM pg_database WHERE datname = current_database())
OR l.database = 0
THEN l.relation::regclass::text
END AS relation_name,
p.transaction,
p.gid,
p.prepared,
p.owner
FROM pg_locks l
JOIN pg_prepared_xacts p
ON l.virtualtransaction = '-1/' || p.transaction
WHERE l.pid IS NULL;

pg_prepared_xacts 这一侧也值得单独查:prepared 时间很长、owner 已离职、database 对不上,都是持锁不释放的常见来源。

7. spectoken 属于谓词锁管理器

locktype='spectoken'(以及 SSI 相关的 SIReadLock)来自谓词锁管理器,和常规锁管理器是两个独立系统。谓词锁不造成常规锁阻塞,不会出现在「谁挡谁」的阻塞链里;序列化冲突导致的 40001 也不会在锁等待曲线上出现。排查 40001 要看业务事务的读写依赖和 pg_stat_database.conflicts,不要拿 pg_locks 的 granted=false 当指标。

8. 用 pg_blocking_pids(),不要手写 self join

文档明确说:把 pg_locks 与自身 join 得到「谁阻塞谁」是可能的,但细节正确非常困难——查询必须编码哪些锁模式与哪些模式冲突;该视图不暴露哪个进程在等待队列里排在谁前面,也不暴露哪个进程是为哪个客户端会话运行的并行 worker。更好的做法是用 pg_blocking_pids() 找出等待进程被谁挡住。

Lock Monitoring wiki 给过一个 self-join 模板:对 locktype/database/relation/page/tuple/virtualxid/transactionid/classid/objid/objsubid 全部 IS NOT DISTINCT FROM 匹配,加 blocking_locks.pid != blocked_locks.pid,再过滤 WHERE NOT blocked_locks.granted。这个模板多数情况可用,但仍有正确性问题(尤其 9.6),原因就是上面几条。

把 pg_blocking_pids() 展开成边,LEFT JOIN 处理 blocker 缺失:

SELECT
w.pid AS waiter_pid,
w.wait_event_type,
w.wait_event,
w.query AS waiter_query,
b.pid AS blocker_pid,
b.state AS blocker_state,
b.query AS blocker_query
FROM pg_stat_activity w
LEFT JOIN LATERAL unnest(pg_blocking_pids(w.pid)) AS x(pid) ON true
LEFT JOIN pg_stat_activity b ON b.pid = x.pid
WHERE w.wait_event_type = 'Lock'
ORDER BY w.pid;

LEFT JOIN LATERAL ... ON true 保证即使 pg_blocking_pids() 返回空数组,等待者那一行也会出现,blocker_pid 为 null。这比内连接更接近现场:blocker 可能已经结束,或者是一个 prepared transaction。

pg_blocking_pids() 也有边界:它不告诉你等待队列的先后顺序,也不把并行 worker 归到客户端会话上。需要这些信息时,结合 pg_stat_activity 的 backend_type、leader_pid 和 query_start 一起看。

默认值与边界

  • log_lock_waits 默认 off。打开后借用死锁检测逻辑,只记录等待超过 deadlock_timeout(默认 1s)的锁。日志形如:
LOG: process 663 still waiting for ExclusiveLock on advisory lock [233136,1,2,2] after 1000.365 ms
DETAIL: Process holding the lock: 660. Wait queue: 663.
STATEMENT: SELECT pg_advisory_lock(1, 2);

间歇性锁问题靠这个日志事后分析,比反复轮询 pg_locks 有效。用 SET application_name 标记逻辑调用方,日志和 pg_stat_activity 才对得上。

  • lock_timeout 和 statement_timeout 不是一回事:lock_timeout 只在等待锁的瞬间计时,超时抛 55P03(lock_not_available);statement_timeout 计整条语句的执行时间,超时抛 57014。要限制「等锁」用前者。

  • pg_locks 对普通用户可读,不像 pg_stat_activity.query 会被权限遮蔽或截断。一整张 pg_locks 贴进工单,会把库名、对象名、连接规模都带出去。只贴筛选后的等待行,或先用 database、relation 过滤。

  • 大系统上 pg_locks 可能有数千行,不要频繁全量跑。DBAtlas 的配方用 coalesce(w.relation::regclass::text, 'xid '||w.transactionid, w.virtualxid, w.objid::text) 作为 target,join pg_locks b ON b.pid = ANY(pg_blocking_pids(w.pid)) 且全部身份列 IS NOT DISTINCT FROM 匹配,只列真正冲突的请求;blocker_holds=false 表示阻塞者还没持有该锁、只是以冲突模式排在队列前面。这个查询需要 pg_read_all_stats 权限,waiting_for 基于 state_change 是近似值。

  • 排查时各视图分工:pg_stat_activity 看谁在运行或等待(pid、backend_type、state、wait_event_type/event、xact_start);pg_blocking_pids(pid) 看谁阻塞;pg_locks 看持有哪些锁(locktype、对象 identity、mode、granted);pg_prepared_xacts 看 prepared transaction(transaction、prepared、owner、database);backend_xid/backend_xmin 看当前 backend 的 XID/XMIN。query 文本可能含敏感数据、被截断或因权限不可见;取消或终止 backend 有副作用,先绑定 exact PID + backend start + application/user/database。

互斥与失效点

自连接在四处给出错误答案:

  1. fastpath:fastpath=true 的行不与任何锁冲突,自连接按身份列匹配会造出不存在的 blocker 边;同一次查询里 fastpath 行与主锁表快照不同源,可能自相矛盾。blocker 的 fastpath 为 true 时,这条边可以直接丢掉。

  2. 行锁:locktype='transactionid' 的等待不是对象锁等待,是等事务结束。自连接能匹配到 transactionid 这一行,但匹配不到「行」,于是把等待归因到事务 ID 上;如果同时有多个事务持有该 ID 的共享锁,还会把不相关的持有者当成 blocker。

  3. 跨库 OID:relation 列 join pg_class 只在当前库/共享对象正确。自连接模板按 database 匹配能避开一部分,但只要输出里用 relation::regclass 或 join pg_class 取表名,跨库同 OID 就会错。用 database 列过滤,或 IN (...) 显式限定 datname。

  4. prepared transaction:pid 为 null,自连接的 blocking_locks.pid != blocked_locks.pid 和 b.pid = ANY(pg_blocking_pids(...)) 都可能把它漏掉。它不等待,但一直持锁;单独 join pg_prepared_xacts(virtualtransaction = '-1/' || transaction)。

还有一条不是「错答案」但会误导:pg_locks 同时包含常规锁和谓词锁,两个管理器不是同时加锁采集的。spectoken 行不参与常规阻塞,别算进等待链。

可执行判据

先跑等待行,再按结果分支,不要一上来拉全表:

SELECT
w.pid,
w.locktype,
w.mode,
w.waitstart,
now() - w.waitstart AS waited,
pg_blocking_pids(w.pid) AS blocker_pids,
CASE
WHEN w.database = (SELECT oid FROM pg_database WHERE datname = current_database())
OR w.database = 0
THEN w.relation::regclass::text
END AS relation_name
FROM pg_locks w
WHERE NOT w.granted
ORDER BY w.waitstart NULLS LAST;

判据顺序:

  1. 这条查询有行且 waited 持续增长 → pg_blocking_pids(w.pid) 非空,去 pg_stat_activity 看 blocker 的 state、query、xact_start。
  2. blocker_pids 为空,但等待行还在 → 查 pg_prepared_xacts 里 prepared 时间长的条目,用 virtualtransaction = '-1/' || transaction 对回 pg_locks。
  3. 等待行 locktype='transactionid' → 去 pg_stat_activity 找 backend_xid、xact_start 和未提交事务,不要在 pg_locks 里找行。
  4. 需要判断锁总量是否失真时,单独跑 count(*) FILTER (WHERE fastpath);total_rows 高不代表锁竞争严重。
复制全文 生成海报 pg_locks PostgreSQL 锁等待 数据库 运维 SQL

推荐文章

程序员茄子在线接单