PostgreSQL 的锁:为什么你的 ALTER TABLE 会卡住,以及怎么查

上周同事问我,为什么给一张表加个字段,整个表就像被冻住了一样,连普通的 SELECT 都进不来。其实这就是 PostgreSQL 的锁在起作用。

很多人用 PG,觉得它 MVCC 很厉害,读写互不阻塞。这没错,但 MVCC 主要解决的是“读”和“写”之间的冲突。真到了写和写、DDL 和 DML 之间,还是得靠锁来协调。今天就把 PG 里常见的锁捋一遍,顺便说说怎么排查锁等待和死锁。

先搞清楚:普通 SELECT 到底加不加锁?

这是最容易误解的地方。在 PostgreSQL 里,普通的 SELECT 只会在表上加一个 ACCESS SHARE 锁,这个锁非常弱,唯一会跟它冲突的就是 ACCESS EXCLUSIVE。也就是说,你平时查询,基本不会阻塞别人,别人也很难阻塞你。

真正会打架的是写操作和 DDL。比如 ALTER TABLE、DROP TABLE、TRUNCATE 这些通常会加 ACCESS EXCLUSIVE,这个锁一出来,所有其他访问都得排队。

所以,那个同事遇到的“加字段卡住”,大概率是 ALTER TABLE 在等 ACCESS EXCLUSIVE,而前面有长事务或者慢查询占着表,导致后面所有请求都堵住了。

表级锁

PostgreSQL 表级锁有 8 种模式,强度从弱到强排下来是:

  1. ACCESS SHARE
  2. ROW SHARE
  3. ROW EXCLUSIVE
  4. SHARE UPDATE EXCLUSIVE
  5. SHARE
  6. SHARE ROW EXCLUSIVE
  7. EXCLUSIVE
  8. ACCESS EXCLUSIVE

兼容矩阵我贴在下面,X 表示冲突,空白表示兼容:

请求\持有 ACCESS SHARE ROW SHARE ROW EXCLUSIVE SHARE UPDATE EXCLUSIVE SHARE SHARE ROW EXCLUSIVE EXCLUSIVE ACCESS EXCLUSIVE
ACCESS SHARE X
ROW SHARE X X
ROW EXCLUSIVE X X X X
SHARE UPDATE EXCLUSIVE X X X X X
SHARE X X X X X
SHARE ROW EXCLUSIVE X X X X X X
EXCLUSIVE X X X X X X X
ACCESS EXCLUSIVE X X X X X X X X

这张表不用背,记住几个关键就行:

  • ACCESS SHARE 是最弱的,普通 SELECT 拿的就是它。它只跟 ACCESS EXCLUSIVE 冲突。
  • ROW EXCLUSIVE 是 INSERT/UPDATE/DELETE 拿的,它跟 SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE 冲突。
  • ACCESS EXCLUSIVE 是老大,跟所有锁都冲突。ALTER TABLE、DROP TABLE、TRUNCATE、VACUUM FULL、CLUSTER、REINDEX 这些基本都会拿它。
  • CREATE INDEX CONCURRENTLY 比较特殊,它拿的是 SHARE UPDATE EXCLUSIVE,所以不会阻塞 DML,这也是大表建索引推荐它的原因。

我一般会提醒团队:DDL 之前一定要设 lock_timeout。不然一个 ALTER TABLE 可能因为等锁排到所有查询后面,把整个库拖慢。

SET lock_timeout = '3s';
ALTER TABLE users ADD COLUMN age int;

如果 3 秒拿不到锁,它自己就放弃了,不会一直堵着。

行级锁

行级锁是写操作在行上加的锁。普通 SELECT 不加行锁。四种行锁:

  • FOR KEY SHARE
  • FOR SHARE
  • FOR NO KEY UPDATE
  • FOR UPDATE

强度从左到右递增。兼容矩阵:

请求\持有 FOR KEY SHARE FOR SHARE FOR NO KEY UPDATE FOR UPDATE
FOR KEY SHARE X
FOR SHARE X X
FOR NO KEY UPDATE X X X
FOR UPDATE X X X X

几个实际场景:

  • UPDATE 通常拿 FOR NO KEY UPDATE。如果更新的是唯一键列,可能会升级成 FOR UPDATE。
  • DELETE 通常拿 FOR UPDATE。
  • SELECT ... FOR UPDATE 显式拿最强的行锁,用来做悲观锁。
  • SELECT ... FOR SHARE 拿共享锁,别人可以读,但不能改。
  • SELECT ... FOR KEY SHARE 最弱,外键检查时经常用。比如子表插入一条记录,会去父表对应行上加 FOR KEY SHARE,防止父表键被删掉或改掉。

行锁信息存在元组头的 xmax 里。多个事务锁同一行时,PG 会用 MultiXact 来记录。行锁在事务结束时释放,所以长事务会一直占着行锁,这是很多锁等待的根源。

一个常见坑:外键会让子表写入阻塞父表键更新。比如你更新父表的主键,子表可能正在插入,两边就会互相等。设计外键时心里要有数。

锁等待和死锁,怎么查?

PG 提供了几个视图,最常用的是 pg_locks 和 pg_stat_activity。

查当前没拿到的锁:

SELECT pid, locktype, relation::regclass, mode, granted, query
FROM pg_locks l
LEFT JOIN pg_stat_activity a USING (pid)
WHERE NOT granted;

查谁被谁阻塞了:

SELECT pid, pg_blocking_pids(pid) AS blocking_pids, query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;

pg_blocking_pids 这个函数特别好用,直接告诉你哪个进程堵住了当前查询。

如果要杀查询,有两个选择:

SELECT pg_cancel_backend(pid);   -- 取消当前查询,连接还在
SELECT pg_terminate_backend(pid); -- 直接断开连接

一般先 cancel,不行再 terminate。

死锁是另一个话题。PG 有 deadlock_timeout,默认 1 秒。如果一个事务等锁超过这个时间,就会触发死锁检测。检测到循环等待,它会杀掉其中一个事务,报 deadlock detected,应用应该捕获这个错误并重试。

死锁没法完全避免,但可以降低概率:

  • 事务尽量短,别在事务里等用户输入。
  • 多个表更新时,固定访问顺序。
  • 用 SELECT ... FOR UPDATE NOWAIT 或 SKIP LOCKED 避免无限等待。

比如队列消费场景,SKIP LOCKED 就很好用:

SELECT * FROM jobs
WHERE status = 'pending'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED;

这样多个 worker 可以同时取任务,不会互相等。

咨询锁:应用层的互斥

除了表锁和行锁,PG 还有咨询锁(Advisory Lock)。它不绑定任何数据库对象,纯粹是应用自己约定的一把锁。

会话级:

SELECT pg_advisory_lock(12345);
SELECT pg_advisory_unlock(12345);

事务级:

SELECT pg_advisory_xact_lock(12345);

事务级会在事务结束时自动释放,用起来更省心。咨询锁适合做“同一时间只有一个任务跑”这种场景,比如定时任务、分布式锁的简易实现。但注意,它只在同一个数据库集群内有效,跨实例不行。

几个实践里踩过的坑

  1. 长事务是万恶之源。一个事务开着不提交,行锁不释放,VACUUM 也清不掉死元组。监控 pg_stat_activity 里 state = 'idle in transaction' 的会话。
  2. DDL 一定要加 lock_timeout。尤其是 ALTER TABLE,不加锁超时,它可能排在一个长查询后面,把后面所有请求都堵死。
  3. 大表建索引用 CREATE INDEX CONCURRENTLY。普通 CREATE INDEX 会拿 SHARE 锁,阻塞写。并发建索引虽然慢一点,但不阻塞 DML。
  4. VACUUM FULL 和 CLUSTER 会拿 ACCESS EXCLUSIVE,生产环境慎用,最好在低峰期做。
  5. 隔离级别要心里有数。READ COMMITTED 是默认,每个语句一个快照。REPEATABLE READ 和 SERIALIZABLE 可能报序列化失败,应用要有重试逻辑。
  6. 监控锁等待。可以定期查 pg_locks 里 granted = false 的记录,或者用 pg_blocking_pids 看阻塞链。

最后说一句

PostgreSQL 的锁并不复杂,复杂的是不知道谁在等谁。MVCC 让读不阻塞写,但写写冲突、DDL 冲突还是得靠锁。遇到卡顿,先看 pg_locks 和 pg_stat_activity,找到阻塞源头,再决定是等、是取消,还是优化事务。

锁不是敌人,它只是并发控制的工具。理解它,你就能少踩很多坑。

posted @ 2026-09-26 17:31  Y00  阅读(33)  评论(0)    收藏  举报