1. 深入理解pg_blocking_pids()函数
PostgreSQL数据库中的pg_blocking_pids()函数是一个极其有用的诊断工具,它能帮助我们快速识别哪些会话正在阻塞其他会话。在实际生产环境中,锁等待和阻塞问题是影响数据库性能的常见因素,这个函数就像数据库管理员口袋里的瑞士军刀,能快速定位问题源头。
我第一次遇到这个函数是在处理一个紧急的生产事故时。当时应用突然变慢,前端请求大量超时,通过pg_blocking_pids()迅速锁定了是一个长时间运行的分析查询阻塞了关键事务。这种实战价值让我对这个函数产生了浓厚兴趣。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 函数基础解析
2.1 函数定义与语法
pg_blocking_pids()函数的完整语法如下:
sql复制pg_blocking_pids(pid integer) → integer[]
这个函数接收一个PostgreSQL后端进程ID(pid)作为参数,返回一个整数数组,包含所有当前正在阻塞该进程的其他进程ID。如果没有阻塞,则返回空数组。
2.2 返回值解读
理解返回值有几个关键点:
- 返回的是直接阻塞者,不是整个阻塞链
- 数组顺序不代表阻塞顺序
- 结果实时变化,每次调用都可能不同
3. 典型应用场景
3.1 锁等待问题诊断
最常见的应用场景是诊断锁等待。假设我们有一个会话(pid=123)长时间处于"idle in transaction"状态,可以通过以下查询检查它是否被阻塞:
sql复制SELECT pg_blocking_pids(123);
如果返回非空数组,比如{456},说明pid=456的会话正在阻塞123。
3.2 结合其他系统视图使用
单独使用pg_blocking_pids()信息有限,通常需要结合pg_stat_activity视图获取更全面的信息:
sql复制SELECT blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.usename AS blocked_user,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_query,
blocking_activity.query AS blocking_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.GRANTED;
这个复杂查询实际上可以用pg_blocking_pids()简化:
sql复制SELECT pid,
usename,
pg_blocking_pids(pid) AS blocked_by,
query,
query_start,
state_change,
state
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
4. 实战技巧与注意事项
4.1 阻塞链分析
pg_blocking_pids()只返回直接阻塞者,但实际可能存在阻塞链。要分析整个链条,可以使用递归查询:
sql复制WITH RECURSIVE blocking_chains AS (
SELECT pid, pg_blocking_pids(pid) AS blockers, ARRAY[pid] AS chain, 1 AS level
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) = 0
UNION ALL
SELECT a.pid, pg_blocking_pids(a.pid), bc.chain || a.pid, bc.level + 1
FROM pg_stat_activity a
JOIN blocking_chains bc ON bc.pid = ANY(pg_blocking_pids(a.pid))
)
SELECT pid, blockers, chain, level
FROM blocking_chains
ORDER BY level, pid;
4.2 性能考虑
虽然pg_blocking_pids()很有用,但在高负载系统上频繁调用可能影响性能。建议:
- 不要每秒多次调用
- 在监控脚本中合理设置间隔(如30秒)
- 考虑使用pg_stat_activity的backend_xmin字段识别长事务
4.3 常见误用
新手常犯的错误包括:
- 认为返回的数组顺序代表阻塞顺序(实际没有顺序保证)
- 忽略间接阻塞(需要递归分析)
- 在事务中重复调用并期望相同结果(锁状态可能变化)
5. 高级应用场景
5.1 自动化监控
可以创建自动化监控脚本,定期检查阻塞情况并发出警报:
bash复制#!/bin/bash
BLOCKED_COUNT=$(psql -U postgres -d postgres -t -c \
"SELECT count(*) FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0")
if [ "$BLOCKED_COUNT" -gt 0 ]; then
# 发送警报邮件或Slack通知
psql -U postgres -d postgres -c \
"SELECT now() as time, pid, usename, pg_blocking_pids(pid) as blocked_by, \
query, state FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0" \
| mail -s "PostgreSQL Blocking Alert" admin@example.com
fi
5.2 与扩展结合使用
一些PostgreSQL扩展如pg_stat_statements可以与pg_blocking_pids()结合,提供更深入的性能分析:
sql复制SELECT s.query, count(*) as blocked_count
FROM pg_stat_activity a
JOIN pg_stat_statements s ON a.queryid = s.queryid
WHERE cardinality(pg_blocking_pids(a.pid)) > 0
GROUP BY s.query
ORDER BY blocked_count DESC;
6. 内部实现解析
6.1 底层原理
pg_blocking_pids()函数内部查询pg_locks系统目录,检查哪些进程持有与目标进程请求冲突的锁。PostgreSQL使用多版本并发控制(MVCC),但某些操作仍需要锁,如表锁、行锁等。
6.2 锁类型考虑
函数会检查所有锁类型,包括:
- 关系锁(表锁)
- 元组锁(行锁)
- 事务ID锁
- 咨询锁
但不会考虑等待缓冲区pin或IO等待等非锁阻塞。
7. 替代方案比较
7.1 与pg_locks视图对比
直接查询pg_locks可以获得更详细的锁信息,但更复杂:
sql复制SELECT blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.query AS blocked_query,
blocking_activity.query AS blocking_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.GRANTED;
pg_blocking_pids()封装了这种复杂性,提供了更简单的接口。
7.2 与pg_stat_activity的wait_event对比
PostgreSQL 9.6+引入了wait_event字段,可以显示会话在等待什么,但不直接显示谁在阻塞它。两者可以互补使用:
sql复制SELECT pid, wait_event_type, wait_event, pg_blocking_pids(pid)
FROM pg_stat_activity
WHERE wait_event IS NOT NULL;
8. 性能优化建议
8.1 减少锁等待
基于pg_blocking_pids()的发现,可以采取以下优化措施:
- 缩短事务持续时间
- 将大事务拆分为小事务
- 调整锁获取顺序(避免交叉锁)
- 使用较低的隔离级别(如READ COMMITTED代替SERIALIZABLE)
8.2 查询优化
某些查询模式容易导致阻塞:
- 无索引的外键更新
- 全表扫描的UPDATE/DELETE
- 长时间运行的VACUUM
通过pg_blocking_pids()识别这些问题后,可以添加适当索引或调整查询。
9. 实际案例分享
9.1 案例一:外键约束导致的阻塞
在一次生产问题中,应用出现间歇性超时。使用pg_blocking_pids()发现多个会话被同一个会话阻塞。进一步分析发现是外键约束缺少索引,导致引用表的更新需要全表扫描并持有锁。
解决方案是为外键列添加索引:
sql复制CREATE INDEX CONCURRENTLY idx_order_customer_id ON orders(customer_id);
9.2 案例二:应用连接池配置不当
另一个案例中,pg_blocking_pids()显示多个会话被一个"idle in transaction"会话阻塞。原因是应用连接池配置不当,连接归还时没有正确结束事务。
解决方案是调整连接池配置,确保归还连接前提交或回滚事务。
10. 最佳实践总结
经过多年使用pg_blocking_pids()的经验,我总结了以下最佳实践:
- 监控系统中定期运行阻塞检查(间隔30秒到1分钟)
- 对发现的阻塞问题记录历史,分析模式
- 结合pg_stat_statements识别频繁导致阻塞的查询
- 为常见阻塞场景编写自动化处理脚本
- 在应用开发阶段就考虑并发控制策略
记住,pg_blocking_pids()只是诊断工具,解决阻塞问题需要深入理解应用工作负载和PostgreSQL的并发控制机制。
