1. 锁竞争排查的核心思路
1.1 数据库“卡住”了,从哪下手?
做 PostgreSQL 运维或者开发的同学,肯定都遇到过这种情况:一条简单的 UPDATE 或者 SELECT 突然就跑不动了,应用侧一直转圈,监控面板上的活跃会话数直线上升,业务反馈“数据库响应变慢”。这时候去数据库里查pg_stat_activity,能看到一大堆 state 为active的会话,但对应的 SQL 其实什么都没干,就这么干等着。
这种“看似活跃、实则等待”的会话,绝大多数情况下都是被锁阻塞了。PostgreSQL 的锁机制不像某些数据库那样用超时或排队策略自动处理,它采用的是“阻塞即等待”的模型:一个事务拿到了某行或某表的锁,另一个事务想拿同一把锁,就只能乖乖排在后面,等待持有锁的事务提交或回滚。如果持有锁的事务一直不结束,后面等待的事务就会无限期挂起,应用侧的连接池很快被占满,整个系统就这样被拖垮。
问题来了:怎么定位到底是谁在阻塞?这条被卡住的 SQL 是哪个会话持有的锁导致的?在没有专门工具的前提下,很多人的第一反应是查pg_locks,把这表里所有锁的持有关系和等待关系一条条列出来,再手工对照pg_stat_activity去拼 PID、拼事务 ID。说实话,这套做法我现在还在用,但它非常繁琐,尤其当系统里有几十个会话在互相牵扯的时候,光靠人眼看pg_locks去判断“谁等谁”简直是一场灾难。
PostgreSQL 9.6 引入的pg_blocking_pids()函数,就是为了解决这个痛点。它干的事很简单:传入一个正在等待的进程 PID,它直接把阻塞这个 PID 的所有后端进程 PID 以数组形式返回。比如你查到某条 SQL 的 PID 是 12345,执行SELECT pg_blocking_pids(12345);,得到{9876},就意味着 PID 9876 这个会话正在阻塞它。一次函数调用,阻塞关系一目了然。
1.2 这个函数能解决什么问题
pg_blocking_pids()的核心价值,是把“需要多表关联加人工分析的锁等待定位”压缩成了一个函数调用。它解决的典型问题包括:
- 业务侧反馈某个功能响应慢,需要快速定位是不是数据库锁竞争导致的;
- 监控系统检测到活跃会话堆积,需要批量输出每个被阻塞会话对应的“元凶”PID;
- 自动化运维脚本需要判断某个会话是否可以安全 kill,避免误杀;
- 排查死锁或长时间锁等待时,需要在多个会话之间理清阻塞链条。
这个函数在 PostgreSQL 9.6 及以上版本都可用,目前主流的 12、13、14、15、16 版本完全覆盖。如果你的生产环境还在用 9.5 或更早的版本,那只能老老实实回去查pg_locks了——但我建议你尽快升级,不光是这个函数的问题,老版本的性能和安全性都差太远了。
1.3 和传统 pg_locks 排查方式对比
我最早排查锁等待,用的是纯pg_locks方案,大致思路是:
SELECT blocked.pid AS blocked_pid, blocker.pid AS blocker_pid FROM pg_locks blocked JOIN pg_locks blocker ON blocker.locktype = blocked.locktype AND blocker.database IS NOT DISTINCT FROM blocked.database AND blocker.relation IS NOT DISTINCT FROM blocked.relation AND blocker.pid != blocked.pid WHERE NOT blocked.granted;这段 SQL 核心逻辑是:找到所有未被授予的锁请求(也就是等待锁的会话),然后去pg_locks里找同一把锁上已经持有(granted)的会话。能跑,但有两个硬伤:
一是自连接条件很容易漏。pg_locks里不同 locktype 的匹配字段完全不同,行锁要匹配tuple和classid,页锁要匹配page,事务锁要匹配transactionid,想把所有情况都覆盖到,SQL 会变得非常长且脆弱。
二是它只能找“直接阻塞者”。A 阻塞了 B,B 阻塞了 C,你用这个 SQL 能看到 C 被 B 阻塞,但看不出来根因是 A,你得继续对 B 做一次同样的查询才能往上追溯。
pg_blocking_pids()把这两层问题都简化了——它内部会综合检查所有 locktype 的持有与等待关系,你不需要知道锁的底层细节,只需要知道“这个 PID 在等谁”。虽然它返回的仍然只是“直接阻塞者”,但结合递归查询,可以很轻松地把整条阻塞链挖出来。后面的章节我会给出具体的递归查询写法。
2. 核心细节解析与实操要点
2.1 函数签名与返回值的正确理解
pg_blocking_pids()的官方签名是:
pg_blocking_pids(pid integer) RETURNS integer[]几个容易踩坑的点,我逐个说。
返回值是integer[]数组,不是单个整数。这意味着一瞬间可能有多个会话同时阻塞目标进程,所以返回的是一个 PID 列表。在 psql 里直接执行,你会看到{123,456}这样的输出。如果返回{},说明这个 PID 当前没有被任何其他进程阻塞——注意,没被锁阻塞 ≠ SQL 执行正常,它可能是正常在执行,也可能是在等待 IO、等待客户端输入等。
入参是pg_stat_activity.pid,不是内部事务 ID。有的同学会把这个函数跟pg_backend_pid()搞混。pg_backend_pid()是返回“当前会话自己的”PID,用于在触发器或函数里标识自己;而pg_blocking_pids()是查询“别的人”谁在阻塞某个指定的后端进程。两者一个是查自己、一个是查别人,别用反了。
一个常见误区是:阻塞者和被阻塞者是相对的概念。pg_blocking_pids(123)返回{456},描述的是“123 这个会话在等待 456”;如果 456 同时也在等待别人,比如等待 789,那么 456 既是阻塞者,也是被阻塞者。锁竞争场景中经常出现这种链式等待,甚至环形等待(死锁)。处理的时候要找链条最顶端的那个“根阻塞者”,而不是看到谁就杀谁。
2.2 配合 pg_stat_activity 联合查询的标准姿势
pg_blocking_pids()单独用意义不大,它只告诉你 PID 之间的关系,但 PID 背后是谁、在跑什么 SQL、持锁多久了,这些信息全在pg_stat_activity里。两者必须配合使用。
我最常用的一个诊断查询长这样:
SELECT blocked.pid AS blocked_pid, blocked.query AS blocked_query, blocked.application_name AS blocked_app, blocked.client_addr AS blocked_client, blocked.state AS blocked_state, array_agg(DISTINCT blocker.pid) AS blocker_pids, array_agg(DISTINCT blocker.query) AS blocker_queries FROM pg_stat_activity blocked LEFT JOIN LATERAL ( SELECT pid, query FROM pg_stat_activity WHERE pid = ANY (pg_blocking_pids(blocked.pid)) ) blocker ON true WHERE blocked.state = 'active' GROUP BY blocked.pid, blocked.query, blocked.application_name, blocked.client_addr, blocked.state HAVING array_agg(DISTINCT blocker.pid) IS NOT NULL;走一遍你就会发现,能直接列出所有“当前正在被阻塞的会话”以及对应的“阻塞者PID”和“阻塞者正在执行的SQL”。这个查询跑出来的结果里,通常能一眼看到问题所在:要么是某个应用连接长时间idle in transaction持锁不释放,要么是一条慢 SQL 拖住了事务没提交。
补充一个实用技巧:如果现场会话非常多,输出结果会很乱,我一般加上ORDER BY blocked_pid或者把blocker_queries截断显示:
left(blocker.query, 80) AS blocker_query_preview这样结果集更紧凑,适合快速扫一眼判断现状。
2.3 如何基于返回结果判断锁类型与等待链
pg_blocking_pids()本身不告诉你“等的是什么锁”,但你可以结合被阻塞会话在pg_stat_activity里的wait_event_type和wait_event来推断。这两个字段是 PostgreSQL 9.6 开始提供的,10 之后已经非常稳定了。
当wait_event_type = 'Lock'时,wait_event的值会告诉你具体的锁类型:
| wait_event | 含义 | 典型场景 |
|---|---|---|
| relation | 正在等待表级锁 | 有人持有 ACCESS EXCLUSIVE 锁(比如 ALTER TABLE),你的 DML 被阻塞 |
| extend | 正在等待扩展表文件锁 | 并发 INSERT 到同一张表时,block size 扩展阶段偶发冲突 |
| transactionid | 正在等待行级锁 | 两个事务更新同一行,后者在等前者提交或回滚 |
| tuple | 正在等待行版本可见性确认 | 与 UPDATE/DELETE 的行版本链相关 |
| virtualxid | 正在等待事务自身的虚拟事务 ID 锁 | 通常与并发 INSERT 少量冲突有关 |
举个实际例子:pg_blocking_pids(100)返回{200},然后查pg_stat_activity发现 PID 200 的state = 'idle in transaction',wait_event_type = 'Client',说明 PID 200 这个会话已经开启了事务但没提交,手头还握着一把事务锁;PID 100 在等 transactionid 锁。这时候的处理方式就非常明确了——要么联系业务方让 PID 200 提交或回滚,要么在确认无风险的情况下pg_terminate_backend(200)。
判断阻塞链时,我建议不要直接杀最底层的阻塞者,先往上追。举个例子:
SELECT pid, pg_blocking_pids(pid), query FROM pg_stat_activity WHERE pid IN (100, 200, 300);假设结果是:
pid | pg_blocking_pids | query -----+------------------+------- 100 | {200} | UPDATE ... 200 | {300} | UPDATE ... 300 | {} | SELECT ...那么链条是 300 → 200 → 100,根节点是 300。如果要干预,先确认 300 为什么一直持锁,而不是直接处理 100——因为你把 100 杀了,200 可能还在等 300,问题并没有彻底解决。
3. 实操过程与核心环节实现
3.1 复现一个典型锁阻塞场景
为了把整个过程讲清楚,我建议你在本地测试环境自己复现一次。方法很简单:开两个 psql 会话,模拟两个事务争抢同一行。
会话 A:
BEGIN; UPDATE t_user SET status = 1 WHERE id = 100;不要提交,保持事务打开。此时会话 A 已经拿到了 id = 100 这行上的行级锁。
会话 B:
BEGIN; UPDATE t_user SET status = 2 WHERE id = 100;这条语句会一直卡住,因为它在等待会话 A 释放锁。现在查第三个会话:
SELECT pid, state, query, wait_event_type, wait_event FROM pg_stat_activity WHERE query LIKE 'UPDATE t_user%';你会看到 PID B 那行wait_event_type = Lock、wait_event = transactionid,这正是行锁等待的标志。
接下来用pg_blocking_pids()验证:
SELECT pid, pg_blocking_pids(pid) FROM pg_stat_activity WHERE pid = <会话B的PID>;返回结果应该是{会话A的PID},精准命中。
3.2 完整排查链路:从发现到解决
处理生产隐患时,我的标准流程如下,每一步都写出来给你当模板。
第一步:列出所有被阻塞的会话。用 2.2 节那个联合查询,输出所有处于等待状态的会话及其阻塞者。
第二步:找根阻塞者。对被阻塞 PID 逐个执行pg_blocking_pids(),筛选出“没有被任何人阻塞,但同时阻塞了别人”的会话。这些就是根阻塞者。可以直接用一条 SQL 找出来:
SELECT pid, query, state, age(now() - xact_start) AS xact_age FROM pg_stat_activity WHERE pid IN ( SELECT unnest(pg_blocking_pids(pid)) FROM pg_stat_activity ) AND pid NOT IN ( SELECT pid FROM pg_stat_activity WHERE pg_blocking_pids(pid) <> '{}' );原理是:嵌套查询里面,先对所有 PID 调用pg_blocking_pids(),把返回的所有阻塞者 PID 收集起来;外层再筛选那些本身没被任何人阻塞的 PID,这些就是根节点。
第三步:分析根阻塞者在干什么。重点看三个字段:
state:如果idle in transaction,说明事务开了没提交;xact_start:事务开启时间,和当前时间比对,能算出持锁时长;query:最近执行的 SQL,能看出它做了啥。
第四步:决策。如果根阻塞者是一个正常执行但耗时很长的任务,可以等;如果它是一个遗留的僵尸事务,建议直接终止。
第五步:终止会话收尾。用pg_terminate_backend():
SELECT pg_terminate_backend(<根阻塞者PID>);这个函数对普通用户只能终止自己的会话,但超级用户或具备pg_signal_backend权限的角色可以终止任何会话。生产环境我一般用监控账号操作,权限最小化原则。
关于权限多说一句:pg_terminate_backend()只能终止“后端进程”,对后台 worker 和辅助进程是无效的。如果你发现某个会话怎么杀都杀不掉,先看看它是不是state = 'idle'正在等待客户端连接,或者查一下它是不是并行 worker。并行 worker 通常在并行查询时出现,parent 进程终止后它们会自动退出。
3.3 递归查询实现阻塞链一站式输出
前面反复提到“向上追根”,手工一次一次执行pg_blocking_pids()效率太低。我写了一个递归 CTE,用来一次性输出完整的阻塞链:
WITH RECURSIVE blocking_chain AS ( -- 锚点:找出所有被阻塞的叶子节点(即最下游等待者) SELECT s.pid, '{' || s.pid::text || '}'::int[] AS chain, pg_blocking_pids(s.pid) AS blockers, s.query FROM pg_stat_activity s WHERE pg_blocking_pids(s.pid) <> '{}' UNION ALL -- 递归:把阻塞者追加到链条中 SELECT b.pid, bc.chain || b.pid, pg_blocking_pids(b.pid), b.query FROM blocking_chain bc JOIN LATERAL unnest(bc.blockers) AS blocker_pid ON true JOIN pg_stat_activity b ON b.pid = blocker_pid ) SELECT DISTINCT chain AS blocking_chain, query FROM blocking_chain;这段 SQL 的输出,每一行就是一条完整的等待链,从最下游的等待者一路追溯到最上游的持锁者。chain 列里数组的最后一位,就是根阻塞者。
实际使用的时候注意两点:一是递归 CTE 可能会因为重复的 pid 组合导致结果膨胀,所以外层加了DISTINCT;二是如果环存在(死锁),递归会一直循环,需要加上深度限制或者用CYCLE子句。PostgreSQL 14 及以上版本支持直接在 CTE 后加CYCLE pid SET is_cycle USING path来检测环,如果你用的是 14 以下版本,建议加一个深度计数器来控制递归层数。
WITH RECURSIVE blocking_chain AS ( SELECT s.pid, '{' || s.pid::text || '}'::int[] AS chain, pg_blocking_pids(s.pid) AS blockers, s.query, 1 AS depth FROM pg_stat_activity s WHERE pg_blocking_pids(s.pid) <> '{}' UNION ALL SELECT b.pid, bc.chain || b.pid, pg_blocking_pids(b.pid), b.query, bc.depth + 1 FROM blocking_chain bc JOIN LATERAL unnest(bc.blockers) AS blocker_pid ON true JOIN pg_stat_activity b ON b.pid = blocker_pid WHERE bc.depth < 10 ) SELECT DISTINCT chain, query FROM blocking_chain;加depth < 10就是一个暴力但有效的防环手段。正常业务场景不会出现超过 10 层的锁等待链,如果真出现了,那基本可以断定系统存在严重的锁竞争设计问题,不是一条 SQL 能解决的。
3.4 基于 pg_blocking_pids 的自动化监控脚本
排查是临时的,预防才是根本。生产环境里,我建议大家写一个简单的定时脚本,把“当前是否有根阻塞者”这个指标暴露给监控系统。
比如用 cron 每两分钟执行一次下面的 SQL:
SELECT count(*) FROM pg_stat_activity WHERE pid IN ( SELECT unnest(pg_blocking_pids(pid)) FROM pg_stat_activity ) AND pid NOT IN ( SELECT pid FROM pg_stat_activity WHERE pg_blocking_pids(pid) <> '{}' ) AND state = 'idle in transaction' AND age(now() - xact_start) > interval '5 minutes';这个查询统计的是:被其他会话阻塞、且自己是 idle in transaction、且持锁超过 5 分钟的根阻塞者数量。一旦这个数字大于 0,说明有事务卡了超过 5 分钟不释放锁,需要告警了。阈值你可以根据自己的业务来调,比如核心业务表持锁超过 1 分钟就该告警了,普通报表表可以放宽到 10 分钟。
再进阶一点,可以把超时的根阻塞者直接自动终止,但我不建议一上来就做全自动 kill,风险太高。稳妥的做法是:监控脚本先告警,人工确认后手动处理;等流程跑顺了,再针对特定 application_name 的会话做半自动处理——比如把某个固定应用名的僵尸事务自动终止,其他情况仍然转人工。
4. 常见问题与排查技巧实录
4.1 为什么函数返回空数组但 SQL 还是卡住
这个坑我踩过好几次。某次线上一个报表查询长期不返回,pg_stat_activity里state = 'active',wait_event_type = 'IO',wait_event = 'DataFileRead',看着不像锁的问题。当时我直接调用了pg_blocking_pids(),返回{},一时还以为是函数出了问题或者版本不对。
后来才意识到,pg_blocking_pids()只反映“锁等待”这一类阻塞。如果会话是在等待 IO、等待锁存器、等待扩展、或者其他非锁资源,这个函数一概返回空。所以在实际排查时,一定要先看wait_event_type:
Lock:锁等待,用pg_blocking_pids()能查到阻塞者;IO:磁盘 IO 瓶颈,去查慢盘、IOPS 耗尽、临时文件落盘;Client:等待客户端读取数据,通常是持有锁的一方还没提交;Extension、LWLock等:内部锁竞争,得从参数调优和索引优化入手。
判断方向错了,后面全白费。先看wait_event_type再决定用什么工具,这是排查数据库问题的基本素养。
4.2 死锁检测到死锁自动回滚,还需要手动干预吗
很多人一听“死锁”就紧张,但实际上 PostgreSQL 的死锁检测机制是自动运行的。默认每隔deadlock_timeout(默认 1 秒)检查一次锁等待图,发现存在环形等待时,会选择牺牲其中一个事务来回滚,然后向客户端抛出ERROR: deadlock detected。
所以如果死锁只是偶发,数据库系统自己就能消化掉,不需要人工介入。真正需要人为干预的,是“长时间锁等待”而不是“死锁”——也就是某个事务一直持有锁不释放,导致后续所有需要这把锁的事务全部排队,但又不构成环(因为没有相互等待关系),系统不会自动处理。
我见过最典型的案例:一个开发在 psql 里执行了BEGIN; UPDATE ...;后直接关闭了窗口,没有提交也没有回滚。这个会话在服务端并不会立刻断开,它会变成idle in transaction状态,锁继续持有,所有想更新同一行数据的业务请求全部卡住。这种情况下pg_blocking_pids()能精准定位出这个僵尸连接,然后pg_terminate_backend()直接结束它。
针对这类问题,防患于未然的做法是设置两个参数:
SET idle_in_transaction_session_timeout = '5min'; SET lock_timeout = '10s';第一个参数强制空闲事务 5 分钟后自动回滚,第二个参数让一次性锁等待超过 10 秒就主动放弃。但注意,lock_timeout在业务侧设置时需要谨慎,高并发场景下如果设置太短,业务偶发冲突就会直接报错,反而影响可用性。建议先在非核心实例上验证效果。
4.3 批量处理多个阻塞会话时,如何避免误杀
生产环境最忌讳的,就是“看到阻塞者就杀”。实际操作中要分三个层次来判断:
第一层,看会话是不是长事务。age(now() - xact_start)如果超过几个小时,大概率是僵尸事务;如果只有几秒钟,可能是正常业务操作,需要观察一下再决定。
第二层,看会话的application_name和client_addr。如果是应用服务器的连接池会话,杀掉之后连接池会自动重建,影响可控;如果是数据库管理员的 psql 会话,可能是有人在手工维护,贸然杀掉会打断操作。
第三层,看阻塞者是否在等待其他资源。如果阻塞者本身也是wait_event_type = 'Lock',说明它也在等锁,你杀它没意义,得继续往上追根。
另外强烈建议:终止会话前先记录现场。人肉操作时,可以在 psql 里先执行:
SELECT pid, application_name, client_addr, xact_start, now() - xact_start AS xact_duration, query FROM pg_stat_activity WHERE pid IN (需要终止的PID列表);把结果截图或者记录到日志里再去执行终止命令。这样事后如果业务方来问“为什么我的事务被杀了”,你有据可查。
4.4 从 pg_stat_activity 的 state 判断阻塞会话的可杀性
pg_stat_activity.state字段其实透露了大量信息,配合pg_blocking_pids()使用,效果极佳:
| state | 含义 | 锁持有情况 | 处理优先级 |
|---|---|---|---|
| active | 正在执行 SQL | 可能持锁,也可能只是等待 | 需结合 wait_event 判断 |
| idle | 空闲,等待客户端新指令 | 如果是无事务的 idle,则无锁 | 无需处理 |
| idle in transaction | 事务已开启但未提交/回滚 | 高危,极可能持锁 | 优先处理 |
| idle in transaction (aborted) | 事务内发生错误但未回滚 | 高危,持锁且无法继续执行其他 SQL | 优先处理 |
| fastpath function call | 正在执行并行查询的快路径函数 | 可能持锁 | 结合查询内容判断 |
| disabled | 该 PID 被禁用(少见) | 特殊状态 | 一般不用管 |
我遇到过最麻烦的情况,是idle in transaction (aborted)。这种会话属于“卡死的僵尸事务”:事务中一条 SQL 报错了,但应用没有走回滚流程,后续所有 SQL 在这个事务里都无法继续执行,锁却还死死握着。如果业务方没能及时发现,这个会话可以挂一整天。
遇到idle in transaction (aborted)的根阻塞者,基本不用犹豫,直接终止,它就是典型的有百害而无一利:
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle in transaction (aborted)' AND pid IN (根阻塞者列表);4.5 实战速查:锁等待定位速查表
下面这张表是我把多年排查经验压缩成的速查表,放在手边非常实用:
| 场景 | 排查入口 | 关键 SQL | 处理方式 |
|---|---|---|---|
| 单条 UPDATE 卡住 | pg_blocking_pids()查阻塞者 | SELECT pg_blocking_pids(pid); | 终止根阻塞者或等其释放 |
| 大量会话堆积 | 2.2 联合查询 | 列出所有被阻塞会话 | 定位根阻塞者,按 3.2 流程处理 |
| idle in transaction 持锁 | state+ 持锁时长 | age(now() - xact_start) | 超阈值终止会话,并设置idle_in_transaction_session_timeout |
| 多个会话互相等待 | 递归 CTE 查阻塞链 | 3.3 的递归查询 | 找链首,优先终止 |
| 加索引 / ALTER 卡住 | 看wait_event = relation | pg_blocking_pids() | 终止持锁 DML 会话 |
| 并行查询卡住 | 查 parent PID | pg_blocking_pids(pid)为空 | 检查 parent 会话并终止 |
| 死锁报错 | 应用侧收到死锁错误 | 不需要pg_blocking_pids() | 系统自动回滚,优化 SQL 顺序 |
4.6 SQL 层的额外建议:从源头减少锁竞争
排查工具再顺手,也只是“治已病”。如果一个系统频繁出现锁等待,最终还是要回到业务侧和 SQL 侧去优化。
几个我自己的经验:
一是尽量缩短事务持续时间。PostgreSQL 里事务开得越久,持锁越久,被阻塞的概率越大。业务代码里不要让一个事务里混着网络请求、外部接口调用等耗时操作;事务里只做必要的数据库操作。
二是控制 UPDATE 的影响范围。能走索引过滤就绝不走全表扫。全表扫意味着可能拿上 ACCESS EXCLUSIVE 锁,这种锁一拿,连 SELECT 都要被阻塞。
三是批量更新时注意排序。多个进程同时更新同一张表的不同行,如果更新顺序不一致就可能在极低概率下产生死锁。统一按主键排序可以显著减少死锁的概率。
四是合理设置事务隔离级别。READ COMMITTED是默认值,适合绝大多数业务;如果用了REPEATABLE READ或SERIALIZABLE,虽然并发一致性更强,但锁竞争范围也更广,需要更强的运维保障。
五是用逻辑复制或分区表拆分热点。某些高并发场景下,单表单行更新是绝对瓶颈,怎么调 SQL 都没用,只能从架构层面把数据拆开。
5. 从锁排查到锁治理的落地延伸
5.1 将 pg_blocking_pids 融入日常巡检
有很多团队是“出事才查”,平时对数据库的锁状态完全不闻不问。我建议把锁等待巡检做成一个日常任务:每 5 分钟跑一次“根阻塞者数量”查询,超过阈值就告警。这样就能在锁等待发生的最早期去介入,而不是等业务反馈“系统崩溃了”才半夜爬起来排查。
另外,巡检出的历史数据可以沉淀下来:哪个时间段锁冲突最多、哪张表最常被锁、哪个应用最容易持锁超时。这些数据积累一段时间后,你能非常明确地知道下一步应该去优化哪张表的索引、调整哪个应用的连接池参数。
5.2 锁监控与连接池、限流策略的配合
锁问题是数据库的“果”,连接池管理才是很多场景下的“因”。应用侧连接池太小,一旦有几个会话被锁阻塞,连接池会迅速被占满,其他健康请求也挤不进来,形成连锁反应。所以有了锁监控之后,最好和连接池监控联动:锁等待告警触发时,同时观察连接池使用率、活跃连接数、等待队列长度,综合判断问题的影响范围。
我自己实践过的一个有效策略是:数据库侧做“慢持锁”检测,应用侧做“短锁超时”兜底。数据库侧每 5 分钟扫一次持锁超过 10 分钟的根阻塞者,应用侧把数据库驱动的 socketTimeout 和事务超时配置好。两边一起守,大部分锁问题都能被提前掐灭,而不是等业务崩溃了才去救火。
5.3 未来扩展:从单实例判断到集群视角
如果你的 PostgreSQL 环境已经做了读写分离或者使用集群方案,锁问题的排查会稍微复杂一些。写节点上产生的锁等待,通过 pg_blocking_pids 可以看到;但备节点上的冲突等待,比如备库 replay 被长查询阻塞,就需要查 pg_stat_activity 里backend_xmin相关的字段了。
对于集群环境,我建议把每台实例的锁等待指标都采集到统一的监控平台,横向对比。这一点做深了,你就能给业务方一个很清晰的视图:某个时间段内,哪台节点因为锁等待导致延迟上升,进而影响了哪个读写分离链路。这已经不仅仅是函数使用层面的事,而是把锁视作系统健康的核心维度之一了。
把pg_blocking_pids()用好,锁竞争这个问题在 PostgreSQL 体系里就不再是黑盒。它把排查从一个繁琐的推理过程降维成了一个直接、确定的函数调用。我在实际生产中见过太多因为一条锁等待没及时发现而拖垮整个系统的案例,有时候卡住系统的甚至是开发人员随手开的一个事务窗口。掌握这个函数,配合一套清晰的处置流程,相当于给数据库加了一层保险——排查也好、自动化运维也好,都能从容很多。