导读:本期聚焦于阳光创作的《PostgreSQL阻塞怎么排查?用pg_blocking_pids快速定位阻塞源头》,敬请观看详情。一条UPDATE执行了十几分钟毫无动静,后面排队的事务越积越多,这种场景十有八九是锁等待引起的。pg_blocking_pids是PostgreSQL自带的系统函数,只要传入被阻塞会话的PID,它就能直接返回持锁者的进程号,省去手工关联pg_locks视图的繁琐过程。本文先讲清函数的返回结构以及软阻塞和硬阻塞的区别,再通过两个会话模拟一条真实的阻塞链,然后给出结合pg_stat_activity一次性输出所有阻塞对的自查SQL,还会分析idle in transaction这类典型元凶的识别方法,最后补充多级等待链的排查思路和日常监控方案,帮你把生产环境的锁问题处理得又快又稳。

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

PostgreSQL阻塞怎么排查?用pg_blocking_pids快速定位阻塞源头

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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0927/62740.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。