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列 joinpg_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,joinpg_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。
互斥与失效点
自连接在四处给出错误答案:
fastpath:
fastpath=true的行不与任何锁冲突,自连接按身份列匹配会造出不存在的 blocker 边;同一次查询里 fastpath 行与主锁表快照不同源,可能自相矛盾。blocker 的fastpath为 true 时,这条边可以直接丢掉。行锁:
locktype='transactionid'的等待不是对象锁等待,是等事务结束。自连接能匹配到transactionid这一行,但匹配不到「行」,于是把等待归因到事务 ID 上;如果同时有多个事务持有该 ID 的共享锁,还会把不相关的持有者当成 blocker。跨库 OID:
relation列 joinpg_class只在当前库/共享对象正确。自连接模板按database匹配能避开一部分,但只要输出里用relation::regclass或 joinpg_class取表名,跨库同 OID 就会错。用database列过滤,或IN (...)显式限定datname。prepared transaction:
pid为 null,自连接的blocking_locks.pid != blocked_locks.pid和b.pid = ANY(pg_blocking_pids(...))都可能把它漏掉。它不等待,但一直持锁;单独 joinpg_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;
判据顺序:
- 这条查询有行且
waited持续增长 →pg_blocking_pids(w.pid)非空,去pg_stat_activity看 blocker 的state、query、xact_start。 blocker_pids为空,但等待行还在 → 查pg_prepared_xacts里prepared时间长的条目,用virtualtransaction = '-1/' || transaction对回pg_locks。- 等待行
locktype='transactionid'→ 去pg_stat_activity找backend_xid、xact_start和未提交事务,不要在pg_locks里找行。 - 需要判断锁总量是否失真时,单独跑
count(*) FILTER (WHERE fastpath);total_rows高不代表锁竞争严重。