想象一下你在办公室,结果所有门都被锁了,因为有个人把钥匙忘在房间里了。PostgreSQL 里的锁其实就差不多这样。如果有个查询或者事务锁住了某个资源,其他想访问同一个资源的操作就只能等它结束。这种情况会导致延迟、冲突,严重的话甚至让系统卡死。
什么时候会有锁?
PostgreSQL 里的锁(Locks)是用来管理并发访问数据的。它们会在这些场景下出现:
- 执行写操作的时候:
UPDATE、DELETE、INSERT。 - 有事务占着资源不放,比实际需要的时间还久。
- 不同事务抢同一个资源时发生冲突。
真实的数据库就像“资源争夺战场”,就算你觉得系统很稳,一个不小心的事务也能把一切“卡死”,就像 Git 里合并失败一样。
分析锁的工具:pg_locks
pg_locks 是 PostgreSQL 的系统视图,能看到当前有哪些锁被事务持有或者在等待。它能帮你搞清楚:“谁在持有锁,谁在等锁?”
pg_locks 主要字段:
locktype:锁的类型(比如relation、transaction、page、tuple)。database:数据库的 ID。relation:表的 ID(如果锁和表有关)。mode:锁的模式(比如RowExclusiveLock、AccessShareLock)。granted:标志,表示锁是不是已经拿到了(true),还是事务还在等(false)。
注意: PostgreSQL 用的是“分级锁模式”。意思是,不同操作会加不同强度的锁(比如读数据用 AccessShareLock,改表结构用 ExclusiveLock)。
例子:查看所有当前锁
SELECT *
FROM pg_locks;
但如果你直接查 pg_locks,信息会太多太杂。我们来点有用的!
例子:还没拿到的锁(也就是事务在等)
SELECT pid, locktype, relation::regclass AS table_name, mode, granted
FROM pg_locks
WHERE NOT granted;
这里发生了什么?
- 我们筛选
granted = false的记录,也就是锁还没拿到。 relation::regclass把表的 ID 转成表名,看起来更直观。
输出大概长这样:
| pid | locktype | table_name | mode | granted |
|---|---|---|---|---|
| 1234 | relation | students | RowExclusiveLock | false |
| 4321 | relation | courses | RowShareLock | false |
这些查询能帮你定位到底是哪张表/资源被锁了,哪个事务可能是元凶。
分析冲突:pg_blocking_pids()
锁还算小事,要是一个事务卡住另一个怎么办?PostgreSQL 提供了个很方便的办法,用 pg_blocking_pids() 函数找“罪魁祸首”。
pg_blocking_pids() 会返回一组进程 ID(pid),这些进程正在阻塞当前事务。
例子:找出正在阻塞别人的事务
SELECT pid, pg_blocking_pids(pid) AS blocking_pids
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
这里发生了什么?
- 我们用
pg_stat_activity视图查系统里活跃的进程。 pg_blocking_pids(pid)会返回每个pid被哪些进程卡住。如果列表不空(长度大于 0),说明这个进程被锁住了。
结果示例:
| pid | blocking_pids |
|---|---|
| 4567 | {1234, 5678} |
| 6789 | {4321} |
比如 pid = 4567 这个事务被 1234 和 5678 卡住了。我们就找到了“元凶”。
终止阻塞进程
当你找到了阻塞别人的进程,可以用 pg_terminate_backend() 把它干掉:
SELECT pg_terminate_backend(1234); -- “干掉”进程 1234
但要小心!强制终止进程会让当前事务的数据回滚。这个操作就像“核按钮”,不到万不得已别乱用。
实战:锁分析场景
假设我们有个大学数据库,里面有 students 和 enrollments 两张表。现在有好几个事务同时往 enrollments 写数据,结果就出现了锁。
- 定位锁:
SELECT pid, locktype, relation::regclass AS table_name, mode, granted
FROM pg_locks
WHERE NOT granted;
- 找出阻塞进程:
SELECT pid, pg_blocking_pids(pid) AS blocking_pids
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
- 解除锁:
强制终止其中一个冲突进程:
SELECT pg_terminate_backend(1234); -- 终止进程 1234
注意: 在“干掉”进程之前,最好先搞清楚为啥会有锁。也许你该重新设计事务逻辑。
常见错误和预防方法
锁经常是因为事务管理不当造成的。比如:
错误: 一个事务拿着锁不干活,啥都不操作(状态是“idle in transaction”)。
解决: 用 pg_stat_activity 盯着事务状态,及时终止“挂起”的事务。
SELECT pid, state, query
FROM pg_stat_activity
WHERE state = 'idle in transaction';
错误: 查询忘了加索引,导致整张表都被锁了。
解决: 优化查询,对常用条件加索引。
“谁等谁”依赖树表格
为了方便排查,你可以画出依赖树,看看哪个事务卡着哪个:
WITH RECURSIVE blocking_tree AS (
SELECT pid, pg_blocking_pids(pid) AS blocked_by
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0
UNION ALL
SELECT a.pid, pg_blocking_pids(a.pid)
FROM pg_stat_activity a
JOIN blocking_tree b ON a.pid = ANY(b.blocked_by)
)
SELECT pid, blocked_by FROM blocking_tree;
结果:
| pid | blocked_by |
|---|---|
| 4567 | {1234} |
| 1234 | {5678} |
| 5678 | {} |
这里能看到,进程 5678 卡着 1234,1234 又卡着 4567。
GO TO FULL VERSION