在PostgreSQL的日常运维中,锁等待是最让人头疼的问题之一。一个事务拿着锁迟迟不提交,后面一串会话排队等待,页面超时、任务堆积接踵而至。要解开这个死结,第一步是回答一个关键问题:到底是谁在阻塞谁。PostgreSQL 9.6之后提供了一个专门的系统函数pg_blocking_pids,它能直接告诉你某个被卡住的会话正被哪些进程挡着,配合pg_stat_activity视图,几分钟内就能把整条阻塞链摸得清清楚楚。

pg_blocking_pids的返回值与软硬阻塞之分
pg_blocking_pids是一个内置的系统函数,函数签名非常简单:传入一个后端进程的PID,返回一个整型数组,数组里的每一个元素都是一个正在阻塞该会话的进程号。如果数组为空,说明这个会话当前没有被任何进程阻塞;如果传入的PID不存在,函数返回NULL。使用前可以先从pg_stat_activity里拿到所有会话的PID。
-- 查看当前所有后端进程的基本信息 SELECT pid, usename, state, wait_event_type, query FROM pg_stat_activity; -- 查询某个会话被谁阻塞 SELECT pg_blocking_pids(12345);
这个函数的价值在于它把pg_locks视图里复杂的锁冲突判断逻辑封装了起来。如果手工排查,你需要理解锁模式之间的冲突矩阵,还要考虑锁等待队列的先后顺序,写出来的关联查询动辄几十行。而pg_blocking_pids直接把两种情况都考虑进去了:一种是硬阻塞,即对方已经持有了和你请求的锁模式相冲突的锁;另一种是软阻塞,即对方虽然还没拿到锁,但排在你前面,而且它申请的锁模式和你冲突。软阻塞在排队场景下非常常见,不理解这一点,排查时就容易误判。
举个例子,三个会话同时申请同一行的行锁。会话A先拿到锁,会话B和C排队等待。此时对C调用pg_blocking_pids,返回的数组里不仅有A(硬阻塞),还可能有B(软阻塞),因为B排在C前面。B自己也是个受害者,但它的存在确实推迟了C拿到锁的时间。函数把这两种角色都列出来,是为了让你看到完整的等待链,而不是只看到最顶端那个持锁者。和另外两种常见手段相比,它的定位如下表所示:
| 排查手段 | 优点 | 局限 |
| pg_blocking_pids | 一步拿到阻塞者PID,自动包含软阻塞 | 只反映执行瞬间的状态,无历史记录 |
| 手工关联pg_locks | 能看到锁类型、锁模式等底层细节 | SQL复杂,容易漏判软阻塞 |
| log_lock_waits | 留下历史日志,便于事后审计 | 需提前开启,只记录超过deadlock_timeout的等待 |
动手模拟:制造并解开一条阻塞链
光讲原理不够直观,我们直接动手造一条阻塞链,把整个排查过程走一遍。准备一张有数据的表,开三个psql会话即可。
第一步:会话A持锁不放
在第一个会话里显式开启事务并更新一行数据,故意不提交。只要事务不结束,它持有的行锁就不会释放:
-- 会话A(假设PID为66120) BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 注意:故意不执行COMMIT,事务持有的锁不会释放
第二步:会话B撞上锁等待
在第二个会话里更新同一行,这条语句会立刻挂起,进入锁等待状态。行锁在等待时表现为等待事件transactionid:
-- 会话B(假设PID为66135) UPDATE accounts SET balance = balance + 100 WHERE id = 1; -- 语句挂起,等待会话A释放行锁
第三步:会话C定位源头
打开第三个会话做诊断。先从pg_stat_activity里找到卡住的会话B,再对它的PID调用pg_blocking_pids:
-- 找到处于锁等待的会话
SELECT pid, usename, state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock' AND pid <> pg_backend_pid();
-- 查询它被谁阻塞
SELECT pg_blocking_pids(66135);
-- 返回结果:{66120}返回的数组{66120}就是答案:会话B被66120阻塞,也就是最开始那个没提交事务的会话A。整个排查只需要两次查询,不用去啃锁模式冲突矩阵。确认源头后,处理方式就清晰了:要么通知业务侧尽快提交或回滚会话A的事务,要么在必要时用pg_cancel_backend(66120)取消它的查询,极端情况下才动用pg_terminate_backend(66120)直接终止会话。终止会话会导致事务回滚,影响面更大,决策前要评估清楚。
一条SQL列出所有阻塞对
生产环境里通常不止一对阻塞,逐个PID去调用函数效率太低。把pg_blocking_pids和pg_stat_activity关联起来,一条SQL就能把当前所有阻塞关系连同双方的语句一起列出来:
SELECT
blocked.pid AS blocked_pid,
blocked.usename AS blocked_user,
now() - blocked.query_start AS blocked_duration,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.usename AS blocking_user,
blocking.state AS blocking_state,
now() - blocking.xact_start AS blocking_xact_age,
blocking.query AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocking
ON blocking.pid = ANY (pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock';这条SQL里有几个细节值得展开。blocked_duration用now()减去query_start算出被阻塞语句等了多久,是判断紧急程度的关键指标;blocking_xact_age反映阻塞者的事务已经开了多久,如果一个事务开了半小时还没结束,大概率是应用代码里忘了提交,或者长事务里夹带了耗时操作。blocking_state同样重要,如果阻塞源头自己的state是idle in transaction,说明它拿着锁在睡觉,这种会话往往是问题的真正元凶,常见于连接池长连接中事务没有正确关闭的场景。
另外要注意WHERE条件里过滤了wait_event_type为Lock,这样可以排除那些因为IO、网络或其他原因等待的会话,让结果聚焦在锁冲突上。如果你想看到软阻塞的完整排队情况,可以去掉这个过滤条件,改用cardinality(pg_blocking_pids(pid)) > 0作为判断,把所有存在阻塞关系的会话都捞出来。
多级等待链、常见坑与日常监控
阻塞关系不总是简单的一对一。会话A阻塞B,B又阻塞C,C再阻塞D,形成一条多级等待链。用上面的关联查询可以拿到所有相邻的阻塞对,把结果按PID串起来就能还原整条链。排查这种链有个经验法则:优先处理链头的会话,也就是那个没有被别人阻塞、却阻塞了一串人的源头。盲目杀掉中间环节往往无济于事,甚至可能引发连锁回滚,让等待中的事务全部报错。
有几个容易踩的坑需要提醒。第一,pg_blocking_pids反映的是函数执行那一瞬间的状态,锁等待关系可能随时变化,诊断时最好多执行几次确认稳定性。第二,普通用户在pg_stat_activity里看不到其他用户会话的query字段,会显示为空,排查生产问题建议使用具备pg_monitor角色权限的账号。第三,遇到并行查询时,函数返回的可能是并行组leader的PID而不是实际持锁的worker进程,需要结合pg_stat_activity的leader_pid字段进一步确认。
与其事后救火,不如提前设防。给业务会话设置lock_timeout,让等锁的语句在指定时间内自动失败,避免无限期挂起;开启log_lock_waits参数,超过deadlock_timeout的锁等待会被完整记录到日志里,为事后分析留下证据。再配合一条定时执行的监控SQL,把阻塞时长超过阈值的会话推送到告警系统,就能把锁问题的响应时间从小时级压缩到分钟级:
-- 阻塞监控:发现等待超过60秒的锁等待即输出告警明细
SELECT
blocked.pid AS blocked_pid,
extract(epoch FROM now() - blocked.query_start) AS wait_seconds,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocking
ON blocking.pid = ANY (pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock'
AND now() - blocked.query_start > interval '60 seconds';总结一下,pg_blocking_pids把锁冲突判断的复杂度封装成了一次函数调用,配合pg_stat_activity可以快速还原阻塞关系。掌握硬阻塞与软阻塞的区别、学会识别idle in transaction的源头会话、理解多级等待链的处理顺序,这三点是用好这个函数的关键。把它固化成日常巡检SQL,锁问题就不再是让人半夜爬起来翻日志的噩梦。
pg_blocking_pidsPostgreSQL锁阻塞排查修改时间:2026-09-27 23:06:24