news 2026/10/2 9:09:27

PostgreSQL锁问题排查全攻略:从pg_locks到阻塞链定位

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL锁问题排查全攻略:从pg_locks到阻塞链定位

每次线上数据库卡死,我脑子里第一个动作永远是同一件事——查锁。Postgres本身对锁的处理已经相当成熟,行锁、表锁、咨询锁分得清清楚楚,可一旦某个query把持着锁不释放,后面几乎所有的写操作都会堵成一串。这个场景虽然不常发生,但每次出现都是线上事故级别的压力,DBA和研发都必须第一时间定位出“持锁的query是谁”。这篇文章想把排查Postgres锁问题的完整打法拆一遍,从原理到SQL,从定位到处理,给和我一样被线上锁问题折磨过的人一份能直接上手用的排查手册。

1. 什么在挡路:先搞懂Postgres里的锁机制

很多人一听到锁就皱眉,觉得锁是性能杀手,但其实锁是Postgres保证数据一致性的根基。真正让系统卡死的,是“锁持有时间过长”或“持锁事务不结束”,而不是锁本身。如果你不理解锁的基本分类,后面看pg_locks视图大概率是懵的,所以我先把锁的底细梳理清楚。

1.1 锁不是敌人,但乱持锁真的要命

Postgres里的锁主要分几类,对应的应用场景完全不同。

表级锁,比如ACCESS EXCLUSIVE,多出现在ALTER TABLE、TRUNCATE、VACUUM FULL这类DDL操作里。它一旦生效,整张表基本等于被单独占用,连SELECT都要排队。

行级锁是最常见的业务锁来源。一条UPDATE或者SELECT ... FOR UPDATE会对目标行加锁,保证同一时刻只有一个人能改这行数据。行级锁粒度小,并发性好,但如果一个事务更新完不提交,持有行锁的时间会无限拉长,其他要更新同一行的事务全部被卡住。

咨询锁则是应用层自己申请的锁,比如用pg_advisory_lock(key)锁一个业务ID。这类锁不挂在任何表的行或列上,排查起来最隐蔽,因为你在业务表里看不到任何异常,但整个应用可能都被它拖住。

三种锁的持锁方式不同,但检测思路一致:先找到锁对象,再找到持有这条锁的事务,最后看这个事务在等什么、在跑什么query。锁对象、持锁进程、执行的SQL,这三者必须串在一起看,才能把问题钉死。

1.2 定位的核心:锁请求的两个状态

在所有锁排查工具里,最重要的概念是granted这个字段。pg_locks里的每一条记录都代表一个锁请求,granted = true表示这个锁已经被该进程拿到手了,granted = false表示这个进程正在排队等待别人释放锁。

你可以把锁队列想象成银行柜台。granted = true的人已经坐进柜台里办事,占着资源不撒手;granted = false的人在大厅排队叫号。我们要抓的“持锁query”,就是那些granted = true、并且导致其他人granted = false的进程。

只看pid往往不够,同一个进程可能持有多个锁,也可能同时等待多个锁。比如一个事务先拿到了A表的行锁,接着去更新B表的某一行,而B表那行被别的事务锁住,于是这个进程既持有锁又在等锁。这种情况在pg_locks里会同时出现granted = true和granted = false的记录,判断时必须看锁对象是否相同。

1.3 用pg_blocking_pids建立阻塞链

Postgres从9.6版本开始提供了一个非常方便的函数pg_blocking_pids(pid)。你传入一个疑似被阻塞的进程号,它会直接返回阻塞这个进程的pid数组,而且数组中至少有一个pid,返回结果才说明该进程处于等待状态。

这个函数本质上就是对pg_locks的封装,但省掉了复杂的自关联查询,推荐优先使用。它返回的只是“直接阻塞者”,不是整条阻塞链。如果A被B阻塞,B又被C阻塞,pg_blocking_pids(A的pid)只会返回B,你要再对B调用一次,才能继续摸到C。理解这一点,排查多级锁才不会被表面现象带偏。

2. 核心细节与实操要点:让锁持有者当场现形

定位锁问题的核心查询集中在pg_stat_activity和pg_locks两个系统视图上。前者告诉你进程在干什么,后者告诉你进程握着什么锁、在等什么锁。两边的信息缺一不可,只查其一很容易判断失误。

2.1 先看pg_stat_activity:每个进程到底在干嘛

pg_stat_activity是排查数据库进程状态的第一站。我最常用的字段就这几个。

字段含义排查价值
pid后端进程号后续cancel或terminate的输入参数
state当前状态重点看active和idle in transaction
wait_event_type等待类型Lock代表锁等待,Client常见于空闲事务
wait_event等待的具体事件等待锁时为relation、tuple等
query正在执行的SQL锁持有者和等待者的真实现场
query_startquery开始时间判断卡了多久,时间越久越紧急
xact_start当前事务开始时间排查长事务的关键字段

state字段最容易迷惑人。active表示进程正在执行SQL,idle表示连接空闲,还有一种最坑的状态是idle in transaction,意思是事务内已经执行过SQL但一直没提交。这种进程当前没在跑任何query,但手里可能牢牢握着一堆行锁,把别人堵死。看到这种状态,基本可以高度怀疑它就是锁持有者。

wait_event_type = Lock时,wait_event会告诉你等在什么对象上。如果等的是表锁,通常是relation;等的是行锁,可能是tuple;等等。结合query字段里正在等待的SQL,就能判断它想干什么事。

2.2 再看pg_locks:锁的全景图

pg_stat_activity只能看到进程本身,看不到锁对象。锁是谁持有、谁在等待,必须去pg_locks里看。这个视图核心字段有这么几个。

  • locktype:锁类型。常见有relation(表锁)、tuple(行锁)、transactionid(事务锁)、virtualxid(虚拟事务锁)、advisory(咨询锁)等。
  • database:锁所在数据库的OID,NULL可能是因为锁不依赖数据库。
  • relation:锁的目标表OID,可以用::regclass转成表名。
  • page和tuple:行锁的目标位置,两者组合能定位到具体数据行。
  • virtualxid和transactionid:事务相关锁标识,每个事务都会持有自己的事务ID锁。
  • mode:锁强度,比如AccessShareLock、RowExclusiveLock、AccessExclusiveLock。
  • granted:上面讲过的关键状态,true为持有,false为等待。

只看单条记录永远不够,要把pg_locks和pg_stat_activity关联起来,或者直接依赖pg_blocking_pids函数判断阻塞关系,效率会高很多。

2.3 几条现成的SQL,直接复制

先跑一个全景快照,把当前不是idle的进程全部列出来。这个查询能让你快速看清数据库里到底有多少活跃进程、谁卡了多久。

SELECT pid, usename, application_name, client_addr, state, wait_event_type, wait_event, now() - query_start AS query_age, query FROM pg_stat_activity WHERE state <> 'idle' ORDER BY query_age DESC;

注意now() - query_start算出的时间间隔,直接按倒序看,卡得最久的排在最上面。如果某个query_age已经好几分钟甚至更久,而它的wait_event_type又是Lock,那基本可以确认这是一个锁等待现场。

再跑一个专门找“谁在阻塞谁”的查询,把阻塞者和被阻塞者一次拉出来。

SELECT blocked.pid AS blocked_pid, blocker.pid AS blocking_pid, now() - blocked.query_start AS blocked_age, blocked.query AS blocked_query, blocker.query AS blocking_query FROM pg_stat_activity AS blocked JOIN pg_stat_activity AS blocker ON blocker.pid = ANY (pg_blocking_pids(blocked.pid)) WHERE blocked.state <> 'idle' ORDER BY blocked_age DESC;

这个查询用ANY (pg_blocking_pids(...))把阻塞关系展开,输出结果里,blocking_query就是你要找的“持锁query”。注意,如果blocker.query显示为NULL,通常意味着track_activities参数没开启,或该进程处于空闲事务状态。

3. 实操过程与核心环节实现:从卡死到解除的全流程

工具原理讲完,接下来用一个我在生产环境真实处理过的场景,把整个排查过程走一遍。你不需要完全照抄我的命令,照着这个思路去套自己的业务,基本能解决绝大多数锁问题。

3.1 一个让我印象深刻的生产案例

有一次业务方反馈,某个订单系统的列表接口大面积超时,数据库CPU却很低,业务上看起来又不像慢查询。一查活动进程才发现,所有写订单的操作全部堆积在active状态,而它们的query_age都在一两分钟以上,正常的接口早该执行完了。

我立刻跑了上文的第一个全景观测SQL,瞬间定位到一批等待锁的UPDATE语句,它们的wait_event_type是Lock,而且都在等待同一个relation。在Postgres里出现大范围集中等待同一个表,往往是表级锁出了问题,基本可以判断有一个大事务或者DDL卡住了整张表。

继续看那些等待锁的进程,它们的blocking_pids都指向同一个进程。顺着那个pid查pg_stat_activity,发现该进程的state是active,正在跑的query是一条没走索引的大范围UPDATE。这条UPDATE更新了十几万行,事务一直没提交,表级锁和行级锁混合在一起,把后面所有写操作全堵死了。

3.2 现场排查三步走

这类问题虽然看着吓人,但排查就三步。

第一步,拍快照。用psql连接数据库,打开扩展显示模式,手动执行全景观测SQL,把当前进程状态和锁等待信息抓下来。

psql -h 你的主机 -U 你的用户 -d 你的数据库 \x on
SELECT pid, usename, state, wait_event_type, wait_event, now() - query_start AS query_age, query FROM pg_stat_activity WHERE state <> 'idle' ORDER BY query_age DESC;

第二步,找阻塞关系。刚刚那条SQL只能看到单个进程的等待事件,未必能看出谁挡了谁。紧接着跑两个关键查询。

SELECT blocked.pid AS blocked_pid, blocker.pid AS blocking_pid, blocked.query AS blocked_query, blocker.query AS blocking_query FROM pg_stat_activity AS blocked JOIN pg_stat_activity AS blocker ON blocker.pid = ANY (pg_blocking_pids(blocked.pid)) WHERE blocked.state <> 'idle';

如果输出中有清晰的blocking_query,直接锁定那条SQL。如果blocking_pids字段结果是空数组,说明这个进程没有被人阻塞,需要去别的方向排查慢查询或IO问题。

第三步,确认源头后,结合业务线判断处理方式。是前台业务,还是后台任务,还是已经跑了几分钟的大清理?不同场景对“这个锁能不能杀”的判断完全不一样。

3.3 解除阻塞的两种武器怎么选

确定了持锁query之后,接下来才是核心难题:这个锁到底怎么解。

Postgres给DBA提供了两个内置函数。

pg_cancel_backend(pid),相当于给那个进程发一个取消当前query的信号。如果被取消的进程正处于active状态,它的SQL会被中断,连接本身还保留着。适合处理那些还在执行的SQL,比如上文的大范围UPDATE。

pg_terminate_backend(pid),一步到位断开整个后端进程,释放它持有的所有锁和事务。适合处理idle in transaction状态的进程,因为这种进程根本没有在跑的query,cancel对它是无效的,只能断开连接。

我当时的处理顺序是:先给持锁事务的pid调用pg_cancel_backend,观察了几秒钟发现锁没释放。原因是那条UPDATE虽然被取消了,但事务本身还没有结束,行锁依然由事务继续持有。这时候只能升级处理,调用pg_terminate_backend直接把持锁事务端掉。

需要提醒的是,pg_terminate_backend不是银弹。如果数据库本身配置了连接池,断开一个后端连接之后,连接池很可能会立即创建一个新连接来补位,如果补位的连接又默认开启了事务去处理同样的SQL,很可能锁问题再次出现。所以每次处理后,都要观察一段时间,确认没有“杀完又堵”的现象。

4. 常见问题与排查要点:我的实战经验总结

锁排查处理过太多次之后,我发现有几个坑反复出现。这里集中写下来,既是给你的提醒,也是我自己的备忘。

4.1 idle in transaction:最冤枉的锁源头

线上最容易制造“无头悬案”的就是idle in transaction。业务代码里开启事务后执行了一条SELECT或UPDATE,后续逻辑在应用层卡住了或者等待外部接口返回,事务一直不提交。从数据库进程看,它不在跑任何SQL,query字段停在最后执行的那句语句上,但事务id和锁全都在。

这种状态特别容易被新手指错过。大家总是盯着active的query看,却忘了xact_start字段。我在排查时只要看到某个进程的xact_start比query_start早了很多,就会多留一个心眼。最好的办法是设置idle_in_transaction_session_timeout,比如设定为5分钟,让这种长事务自动被清理,别让它拖到线上事故爆发。

4.2 多层阻塞:查锁要顺着链条往下摸

前面说过pg_blocking_pids返回的是直接阻塞者。真正的生产环境中,锁排队往往是多米诺式的。A进程在等B,B又在等C,C才是那个持有最底层锁的进程。如果你只看A的blocking_pids是B,就把B杀掉,结果B被杀了,C还在那里,A可能继续等待,问题一点没解决。

我第一次遇到这种嵌套阻塞时,也是一层层往上追。先查A,发现阻塞pid是B;再单独查B,发现B的blocking_pids里有C;查C之后才看到C是一个忘了提交的事务。如果提前不看链条,上来就杀B,不仅白忙活,还可能误伤正常业务。

处理这种多级情况,我的习惯是写一个循环查询脚本,或者用递归CTE把整个阻塞树拉出来。但在紧急时刻,手工顺着blocking_pids逐层往上翻也很快。关键是记住一个原则:目标永远是找到阻塞链条最深处的那个源头,而不是被阻塞进程的直接前驱。

4.3 锁类型复杂时别只看表面

很多新手排查锁只盯着业务表,看到relation类型的锁就以为完事了,结果怎么查都查不出来。比如autovacuum进程有时候会持有短时间的锁,它对应的application_name是autovacuum worker,如果你不小心把它杀掉,可能影响回收空间和统计信息,甚至触发更糟的IO问题。

还有一个容易忽略的是咨询锁。应用里如果用了pg_advisory_lock或者pg_try_advisory_lock,这张锁不会体现在任何业务表的行上,pg_locks里显示的locktype是advisory。排查的时候如果业务进程之间互相调用、彼此等待,但表锁和行锁都找不到嫌疑,可以考虑往advisory锁方向查。

query字段为空的进程也要格外注意。如果track_activities被关闭了,或者进程处于idle状态,query就是空的。这个时候别慌,通过xact_start和client_addr去判断这个连接从哪来、开了多久,仍然可以拼凑出线索。

4.4 提前设好参数,别等事故教你做人

锁问题排查得再多,也不如提前预防。我建议在Postgres配置里提前把几个关键参数调好。

lock_timeout可以设置一个合理的等待上限,比如5秒或10秒。一条SQL如果在锁队列里排队超过这个时间,就直接放弃执行并报错,避免请求无限堆积。statement_timeout也可以给单条SQL设置总执行时长上限,防止SQL跑飞不返回。

idle_in_transaction_session_timeout前面提过,一定要设。连接池和管理工具导致的空闲事务,是线上锁事故第一元凶,有了这个参数等于多了个自动巡警。

业务层面也要尽量缩短事务。不要在事务里做远程调用、消息发送、文件读写这类不确定耗时的操作,该提交就提交。如果需要批量更新大量数据,建议拆成小批次循环执行,每个批次独立事务,不要用一个巨大事务扛到底。批量和事务拆分这招,我实测下来对锁冲突的缓解效果最明显。

最后再分享一个小技巧:锁问题排查不要等到卡死了才做。平时完全可以每小时跑一次锁监控脚本,把会话数、锁等待数、idle in transaction数记录下来,画成趋势图。数据库卡死前通常不是毫无征兆的,锁等待数量会突然暴涨,有监控和没监控完全是两种体验。真到报警那一刻,你已经提前知道该盯谁了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 9:09:27

PostgreSQL锁等待排查:用PID顺藤摸瓜定位锁源

生产环境里突然一堆应用连不上库&#xff0c;后端日志全是 "canceling statement due to conflict with recovery" 或者干脆卡死在获取连接池连接上&#xff0c;十有八九就是锁等待在作祟。这时候你打开pg_stat_activity&#xff0c;会看到一大片wait_event不是Clien…

作者头像 李华
网站建设 2026/10/2 9:09:25

KNN(k-近邻算法)原理与实战:从入门到工程优化

作为机器学习里公认的“最朴素却最有用”的算法之一&#xff0c;KNN&#xff08;k-近邻算法&#xff09;几乎是我跟每个新人都会提到的第一个必须吃透的模型。它不绕弯子&#xff0c;不靠复杂的数学公式&#xff0c;仅凭“距离最近的一群样本决定你的标签”这样一个直白到不能再…

作者头像 李华
网站建设 2026/10/2 9:09:22

基于NSGA-II的水光互补多目标优化调度:建模、Python实现与调试

水光互补调度&#xff0c;业内这两年讨论热度一直很高。光伏出力波动大、随机性强&#xff0c;单独并网对电网冲击明显&#xff0c;水电调节性能好&#xff0c;两者联合运行既能平滑出力曲线&#xff0c;又能提高整体发电收益。但真正落地的时候你会发现&#xff0c;这事儿没那…

作者头像 李华
网站建设 2026/10/2 9:09:17

基于Python与SQLite的个人财务管理系统开发实战

1. 项目设计与技术选型 1.1 需求分析&#xff1a;记账工具到底需要解决什么问题 记账这件事&#xff0c;大多数人坚持不了几天&#xff0c;不是因为懒&#xff0c;而是因为“记账”和“看账”被割裂了。随手在手机备忘录里记了几笔&#xff0c;月底想看开销结构&#xff0c;还…

作者头像 李华
网站建设 2026/10/2 9:09:13

MySQL 5.7升级8.0:从准备到踩坑排查的完整指南

1. 升级之前先搞清楚&#xff1a;你的MySQL到底该不该升、能升到哪 干MySQL这块的同行应该都有感触&#xff1a;系统跑得好好的&#xff0c;最怕听见"升级"两个字。生产环境动数据库&#xff0c;搞不好就是通宵加背锅。但有些情况你躲不过——官方停止维护、安全漏洞…

作者头像 李华
网站建设 2026/10/2 9:08:33

基于Spring Boot的医院医疗仪器管理系统开发实战

设备科最怕的不是仪器突然坏了&#xff0c;而是坏的时候翻不到这台设备的购买日期、维保记录和上次检修报告。我最早接触这个需求时&#xff0c;对方还在用Excel管理全院几千台医疗设备&#xff0c;维修单靠纸质流转&#xff0c;保养提醒完全取决于设备科老师傅的记忆力。后来我…

作者头像 李华