Skip to content

表级锁(ACCESS SHARE / EXCLUSIVE) ​

PostgreSQL 的锁体系分两个层次:表级锁(Table-Level Lock)与行级锁。任何一条 SQL 只要碰到表,PostgreSQL 都会自动先给这张表加一把表级锁,再去处理行。表级锁决定了「两张表上的语句能不能同时跑」,是理解 DDL 阻塞查询、ALTER TABLE 卡住线上业务这类事故的第一把钥匙。

本节把八种表级锁模式、它们各自的获取场景、完整冲突矩阵、LOCK TABLE 显式加锁、以及如何用 pg_locks 定位锁的来源一次讲清。

前置知识

阅读本节前,建议先了解:BEGIN / COMMIT / ROLLBACK、pg_locks 锁信息

基础概念 ​

表级锁是什么 ​

PostgreSQL 的表级锁不是「对整张表的数据上锁」,而是一个存放在共享内存锁表里的标记(struct):它记录「某个事务在某个表对象上持有了哪一种意向」。真正保护数据行的是行级锁,表级锁解决的是「DDL 与 DML 能不能并存」这个更宏观的问题。

举个最小的例子:

sql
SELECT * FROM orders WHERE id = 1;

这条语句不会锁住任何一行,但它必然在 orders 表上持有一把 ACCESS SHARE 锁——因为「读表」这件事必须先声明「我只是看看,不让别人把表结构改掉」。

八种锁模式完整清单 ​

PostgreSQL 有八种表级锁模式,按「锁定强度」从弱到强排:

锁模式英文全称语义冲突强度
ACCESS SHAREAccess Share Lock只读表,最弱的锁只与 ACCESS EXCLUSIVE 冲突
ROW SHARERow Share Lock打算从表里「选中并锁定」若干行与 EXCLUSIVE / ACCESS EXCLUSIVE 冲突
ROW EXCLUSIVERow Exclusive Lock打算修改表里的行(写表)与 SHARE/SHARE ROW EXCLUSIVE/EXCLUSIVE/ACCESS EXCLUSIVE 冲突
SHARE UPDATE EXCLUSIVEShare Update Exclusive Lock表维护类操作(VACUUM/建索引)自冲突,且与 SHARE 及以上冲突
SHAREShare Lock只读且禁止并发写(如建索引扫描)与写表类锁冲突
SHARE ROW EXCLUSIVEShare Row Exclusive Lock比 SHARE 更严,自冲突自冲突,与写表类锁冲突
EXCLUSIVEExclusive Lock最高级的行锁类,只允许 ACCESS SHARE仅与 ACCESS SHARE 兼容
ACCESS EXCLUSIVEAccess Exclusive Lock独占整表,与所有模式冲突与全部模式冲突

记忆要点:

  • 只有 ACCESS SHARE 和 ACCESS EXCLUSIVE 是「对称」的极端:前者与其他 7 种全部兼容(除 AE),后者与全部冲突。
  • SHARE UPDATE EXCLUSIVE 是唯一「自己和自己冲突」的实用锁模式(SHARE ROW EXCLUSIVE 也自冲突),这使得两个 VACUUM 不会同时跑在同一张表上。

表级锁与行级锁的关系 ​

维度表级锁行级锁
存放位置共享内存锁表(pg_locks.locktype='relation')元组头 xmax 字段(pg_locks.locktype='transactionid')
数量每事务每表最多一把可以锁很多行
是否暴露完全暴露部分暴露(行锁体现为事务 ID 锁)
生命周期到事务结束到事务结束

两者的关系是「先表后行」:执行 UPDATE orders SET ... 时,PostgreSQL 第一件事是在 orders 上拿一把 ROW EXCLUSIVE 表锁,成功之后才去逐行加行级锁。如果表锁就被别人挡住了,行锁根本不会开始。

锁的生命周期:持有到事务结束 ​

这是初学者最容易误解的一点:表级锁(以及行级锁)默认持有到整个事务结束,而不是单条语句结束。

text
BEGIN;                          -- 事务开始
UPDATE orders SET status='x'    -- 在 orders 上拿 ROW EXCLUSIVE,直到 COMMIT 才释放
  WHERE id = 1;
-- ... 中间可以干很多别的事 ...
COMMIT;                         -- 此刻才释放所有锁

在 COMMIT 之前的任何时刻,另一个会话的 ALTER TABLE orders ... 都会一直等待。这就是「一个忘了提交的事务也能锁死整张表」的原因。

语法与用法 ​

显式加表锁用 LOCK TABLE:

text
LOCK [ TABLE ] [ ONLY ] 表名 [ * ] [, ...] [ IN 锁模式 MODE ] [ NOWAIT ]

锁模式 ::=
    ACCESS SHARE
  | ROW SHARE
  | ROW EXCLUSIVE
  | SHARE UPDATE EXCLUSIVE
  | SHARE
  | SHARE ROW EXCLUSIVE
  | EXCLUSIVE
  | ACCESS EXCLUSIVE

-- 不写 IN ... MODE 时,默认加最强的 ACCESS EXCLUSIVE
LOCK TABLE 表名;

最简可运行示例:

sql
-- 显式加一把 SHARE 锁,阻止其他会话写入
BEGIN;
LOCK TABLE orders IN SHARE MODE;
text
LOCK TABLE

不指定模式时默认是 ACCESS EXCLUSIVE:

sql
BEGIN;
LOCK TABLE orders;          -- 等价于 LOCK TABLE orders IN ACCESS EXCLUSIVE MODE;
text
LOCK TABLE

查看当前会话持有的表锁:

sql
SELECT
  c.relname    AS relation,
  l.mode,
  l.granted
FROM pg_locks l
JOIN pg_class c ON c.oid = l.relation
WHERE l.locktype = 'relation'
  AND l.pid = pg_backend_pid();
text
 relation |       mode       | granted
----------+------------------+---------
 orders   | AccessShareLock  | t
 orders   | ShareLock        | t
(2 rows)

同一个会话可以对同一张表持有多把不同模式的锁(PostgreSQL 会把它们合并为最强的那把对外体现),上例中该会话最终对外表现为 ShareLock。

详细说明 ​

八种锁模式逐个详解 ​

ACCESS SHARE(最弱):任何 SELECT(不含 FOR UPDATE 系列)都会加它。它的唯一目的是阻止 ACCESS EXCLUSIVE——即「我在读,你别改表结构」。多个 ACCESS SHARE 之间完全不冲突,所以两个 SELECT 永远不会互相阻塞。

sql
-- 只读查询:自动加 ACCESS SHARE,可无限并发
SELECT count(*) FROM orders;
text
 count
-------
  2000
(1 row)

ROW SHARE:SELECT ... FOR UPDATE / FOR NO KEY UPDATE / FOR SHARE / FOR KEY SHARE 会加它。表达的是「我打算锁住部分行」,因此它不与写表的 ROW EXCLUSIVE 冲突(大家都是改行),只与 EXCLUSIVE / ACCESS EXCLUSIVE 冲突。

sql
-- 行锁系列语句:自动加 ROW SHARE 表锁
SELECT * FROM orders WHERE id = 1 FOR UPDATE;

ROW EXCLUSIVE:INSERT / UPDATE / DELETE / MERGE 会加它。它是最常见的写锁,与 SHARE 系列都不兼容——这解释了「建索引时写操作会被挡住」。

sql
-- 写语句:自动加 ROW EXCLUSIVE 表锁
UPDATE orders SET amount = amount + 1 WHERE id = 1;

SHARE UPDATE EXCLUSIVE(名字很有迷惑性):VACUUM(不含 FULL)、ANALYZE、CREATE INDEX CONCURRENTLY、CREATE STATISTICS、REINDEX CONCURRENTLY 会加它。它的设计目标是「既不让别的维护操作并发,也不挡住读写」:它与 ROW EXCLUSIVE(写表)兼容,所以在线建索引时业务写入照常进行。

sql
-- 在线建索引:加 SHARE UPDATE EXCLUSIVE,不阻塞 DML
CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);

SHARE:CREATE INDEX(不带 CONCURRENTLY)会加它。它阻止并发写入(ROW EXCLUSIVE),但允许并发读(ACCESS SHARE)。这就是「普通建索引不阻塞查询,但阻塞写入」的原因。

sql
-- 普通建索引:加 SHARE,阻塞写入但不阻塞读
CREATE INDEX idx_orders_no ON orders (order_no);

SHARE ROW EXCLUSIVE:CREATE TRIGGER 以及部分 ALTER TABLE 形式会加它。它比 SHARE 更严——自冲突,且与写表类锁冲突。它只允许 ACCESS SHARE 并发。

sql
-- 建触发器:加 SHARE ROW EXCLUSIVE
CREATE TRIGGER trg_audit AFTER UPDATE ON orders
  FOR EACH ROW EXECUTE FUNCTION audit_orders();

EXCLUSIVE:REFRESH MATERIALIZED VIEW CONCURRENTLY 会加它。它只允许 ACCESS SHARE(只读)并发,所有写操作都要等待。这个锁很少见,但一旦出现,说明有并发的物化视图刷新在进行。

sql
-- 并发刷新物化视图:加 EXCLUSIVE
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_orders;

ACCESS EXCLUSIVE(最强):与所有模式冲突,包括 ACCESS SHARE。加它的操作包括:大批 ALTER TABLE / ALTER INDEX、DROP TABLE / DROP INDEX、TRUNCATE、REINDEX(非并发)、CLUSTER、VACUUM FULL、LOCK TABLE(默认)、REINDEX TABLE 等。

sql
-- 最强锁:此刻表上任何其他访问(哪怕 SELECT)都要排队
ALTER TABLE orders ADD COLUMN remark text;
text
ALTER TABLE

ACCESS EXCLUSIVE 是「排队堵门」的元凶

ALTER TABLE 排队时,它不仅挡住后来的写操作,还会挡住后来的读操作——因为它们都要拿 ACCESS SHARE,而 ACCESS EXCLUSIVE 与 ACCESS SHARE 冲突。所以一个 ALTER TABLE 排在一个长查询后面时,它会把后面所有查询也一起堵住,形成雪崩。

各条 SQL 自动加什么锁(获取场景速查) ​

SQL自动表锁是否阻塞写是否阻塞读
SELECTACCESS SHARE否否
SELECT ... FOR UPDATE / FOR SHARE 等ROW SHARE否否
INSERT / UPDATE / DELETE / MERGEROW EXCLUSIVE否否
VACUUM(无 FULL)SHARE UPDATE EXCLUSIVE否否
ANALYZESHARE UPDATE EXCLUSIVE否否
CREATE INDEX CONCURRENTLYSHARE UPDATE EXCLUSIVE否否
CREATE STATISTICSSHARE UPDATE EXCLUSIVE否否
REINDEX CONCURRENTLYSHARE UPDATE EXCLUSIVE否否
CREATE INDEX(普通)SHARE是否
CREATE TRIGGERSHARE ROW EXCLUSIVE是否
REFRESH MATERIALIZED VIEW CONCURRENTLYEXCLUSIVE是否
ALTER TABLE(多数形式)ACCESS EXCLUSIVE是是
DROP TABLE / DROP INDEXACCESS EXCLUSIVE是是
TRUNCATEACCESS EXCLUSIVE是是
VACUUM FULLACCESS EXCLUSIVE是是
CLUSTERACCESS EXCLUSIVE是是
REINDEX(非并发)ACCESS EXCLUSIVE是是
LOCK TABLE(默认)ACCESS EXCLUSIVE是是

这张表是排查锁事故的第一对照表:看到「查询被阻塞了」,先想「当前有没有上面带 是 的操作在跑」。

锁冲突矩阵(8 × 8) ​

下表是官方冲突矩阵:行 = 请求的锁模式,列 = 已被持有的锁模式,X 表示冲突(请求方要等待)。

请求 \ 已持有ACCESS SHAREROW SHAREROW EXCLSHARE UPDATE EXCLSHARESHARE ROW EXCLEXCLACCESS EXCL
ACCESS SHAREX
ROW SHAREXX
ROW EXCLUSIVEXXX
SHARE UPDATE EXCLUSIVEXXXXX
SHAREXXXX
SHARE ROW EXCLUSIVEXXXXX
EXCLUSIVEXXXXXX
ACCESS EXCLUSIVEXXXXXXXX

读法示例:

  • 第 8 行(ACCESS EXCLUSIVE)整行都是 X → 它与任何已持有的锁都冲突,必须排队。
  • 第 1 列(ACCESS SHARE)只有第 8 行是 X → 只被 ACCESS EXCLUSIVE 挡住。
  • 第 4 行第 4 列是 X → SHARE UPDATE EXCLUSIVE 自冲突,两个 VACUUM 不能并发。

LOCK TABLE 显式加锁:NOWAIT 与 IN ... MODE ​

LOCK TABLE 的典型用途是「在事务内一次性锁定一批表,保证后续操作的锁顺序一致」,从而避免死锁。

sql
-- 在事务内先锁两张表,避免后续交错加锁造成死锁
BEGIN;
LOCK TABLE orders, order_items IN ROW EXCLUSIVE MODE;
UPDATE orders SET status = 'paid' WHERE id = 100;
UPDATE order_items SET qty = qty WHERE order_id = 100;
COMMIT;

NOWAIT 让锁请求立即失败而不是等待,适合「拿不到就不做」的场景:

sql
-- 拿不到锁立即报错,不排队
BEGIN;
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE NOWAIT;
text
ERROR:  could not obtain lock on relation "orders"

不写 IN ... MODE 就是 ACCESS EXCLUSIVE,且 LOCK TABLE 必须在事务块内(否则等价于给单条语句加锁后立即释放,没有意义):

sql
-- 错误!事务外 LOCK TABLE 没有意义(自动提交后立即释放)
LOCK TABLE orders;
text
WARNING:  there is no transaction in progress
LOCK TABLE

LOCK TABLE ... IN ACCESS SHARE MODE(比默认弱)一般用于「占位」:拿到这把锁意味着当前没有 ACCESS EXCLUSIVE 持有者,可用于探测表是否正被 DDL 操作。

sql
-- 用 ACCESS SHARE NOWAIT 探测是否有 DDL 在跑
BEGIN;
LOCK TABLE orders IN ACCESS SHARE MODE NOWAIT;
COMMIT;

ACCESS EXCLUSIVE 与 ACCESS SHARE 的阻塞关系 ​

这是本节最需要理解透的一组关系:

text
  ACCESS EXCLUSIVE  ──冲突──  ACCESS SHARE
        ▲                          ▲
        │                          │
   ALTER TABLE / DROP        任意 SELECT

为什么 DDL 会阻塞查询?因为 DDL 要改的是表的结构(比如加列),而正在跑的 SELECT 可能已经基于旧结构生成了执行计划。如果不阻塞,就会出现「查询读到一半表结构变了」的不一致。所以 PostgreSQL 选择让 ACCESS EXCLUSIVE 与 ACCESS SHARE 冲突。

sql
-- 会话 A:一个跑得很久的查询,持有 ACCESS SHARE
SELECT pg_sleep(600), count(*) FROM orders;

-- 会话 B:DDL 需要 ACCESS EXCLUSIVE,被 A 挡住
ALTER TABLE orders ADD COLUMN remark text;
text
-- 会话 B 的界面会一直卡住(没有输出),直到会话 A 结束或超时

危险之处在于排队是 FIFO 的:会话 B 在等 A,会话 C 的 SELECT 又排在 B 后面(因为 B 的 ACCESS EXCLUSIVE 已经「占住队列头」),于是一次 DDL 会把后面的读操作全部堵住。这就是为什么线上 DDL 必须配 lock_timeout。

VACUUM / CREATE INDEX / ALTER TABLE 各加什么锁 ​

三个最常被问到的操作,各自的表锁如下:

操作表锁能否并存
VACUUM(无 FULL)SHARE UPDATE EXCLUSIVE可与读写并存,但不与其他 VACUUM 并存
VACUUM FULLACCESS EXCLUSIVE阻塞一切,且会重写整表
CREATE INDEX CONCURRENTLYSHARE UPDATE EXCLUSIVE不阻塞读写,但耗时长、可能失败
CREATE INDEXSHARE阻塞写,不阻塞读
ALTER TABLE ... ADD COLUMN(无默认值)ACCESS EXCLUSIVE阻塞一切(但很快)
ALTER TABLE ... ADD COLUMN DEFAULT x(PG 11+)ACCESS EXCLUSIVE阻塞一切,但不再重写整表
ALTER TABLE ... VALIDATE CONSTRAINTSHARE UPDATE EXCLUSIVE不阻塞读写
sql
-- VACUUM FULL 会重写整表,必须独占
VACUUM FULL orders;
text
VACUUM

PostgreSQL 11+ 的 ADD COLUMN DEFAULT 优化

PostgreSQL 11 之前,ALTER TABLE ... ADD COLUMN ... DEFAULT x 会重写整张表(对大表是灾难)。11+ 改为只记录「默认值元数据」,新行读时才填充,因此这个 DDL 从「分钟级」变成「毫秒级」。但锁级别仍是 ACCESS EXCLUSIVE,依然会短暂阻塞。

用 pg_locks 关联 pg_class 定位被锁对象 ​

pg_locks 的 relation 字段是 OID,必须关联 pg_class 才能看到表名:

sql
SELECT
  l.pid,
  l.locktype,
  l.mode,
  l.granted,
  l.relation::regclass AS relation,
  a.state,
  left(a.query, 60)  AS query
FROM pg_locks l
LEFT JOIN pg_stat_activity a ON a.pid = l.pid
WHERE l.locktype = 'relation'
  AND l.relation = 'orders'::regclass
ORDER BY l.granted, l.pid;
text
  pid  | locktype |     mode      | granted | relation |        state        |              query
-------+----------+---------------+---------+----------+---------------------+----------------------------------
 10231 | relation | AccessShareLock | t     | orders   | active              | SELECT pg_sleep(600), count(*) F
 10288 | relation | AccessExclusiveLock | f | orders | active              | ALTER TABLE orders ADD COLUMN re
(2 rows)

关键读法:

  • granted = t 的是 锁持有者(这里 pid 10231 的 SELECT)。
  • granted = f 的是等待者(这里 pid 10288 的 ALTER TABLE)。
  • mode 直接告诉你冲突性质。

也可以用 l.relation::regclass 直接输出表名(regclass 会自动做 OID → 名字的转换),比手动 JOIN 更简洁:

sql
SELECT pid, mode, granted, relation::regclass AS rel
FROM pg_locks
WHERE locktype = 'relation' AND relation IS NOT NULL
ORDER BY rel::text, granted;

排查「谁阻塞了谁」 ​

pg_blocking_pids(pid) 是官方提供的直接查阻塞者的函数,返回值是被阻塞进程的「阻塞者」PID 数组:

sql
SELECT
  pid,
  pg_blocking_pids(pid) AS blocking_pids,
  state,
  wait_event_type,
  wait_event,
  left(query, 50) AS query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
text
  pid  | blocking_pids | state  | wait_event_type | wait_event |              query
-------+---------------+--------+-----------------+------------+---------------------------------
 10288 | {10231}       | active | Lock            | relation   | ALTER TABLE orders ADD COLUMN r
(1 row)
  • blocking_pids = {10231} 说明 pid 10288 正被 10231 阻塞。
  • wait_event_type = 'Lock' 且 wait_event = 'relation' 说明它卡在表级锁上(而不是行锁的 transactionid)。

用一条查询完整还原「等待链」:

sql
SELECT
  a.pid                              AS blocked_pid,
  a.query                            AS blocked_query,
  b.pid                              AS blocking_pid,
  b.query                            AS blocking_query,
  b.state                            AS blocking_state,
  now() - b.xact_start               AS blocking_xact_age
FROM pg_stat_activity a
JOIN pg_stat_activity b ON b.pid = ANY(pg_blocking_pids(a.pid))
WHERE cardinality(pg_blocking_pids(a.pid)) > 0;
text
 blocked_pid |          blocked_query          | blocking_pid |     blocking_query      | blocking_state | blocking_xact_age
-------------+---------------------------------+--------------+-------------------------+----------------+-------------------
       10288 | ALTER TABLE orders ADD COLUMN r |        10231 | SELECT pg_sleep(600),...| active         | 00:03:12.44
(1 row)

blocking_xact_age 告诉你阻塞者的事务开了多久——这个值很大时通常意味着有人忘了提交。

表锁与事务边界:锁持有到事务结束 ​

用可视化的方式看锁的持有窗口:

sql
-- 会话 A
BEGIN;
UPDATE orders SET amount = 0 WHERE id = 1;   -- 拿到 ROW EXCLUSIVE,持有中
-- 此处不 COMMIT
sql
-- 会话 B:立刻执行,会一直等待
ALTER TABLE orders ADD COLUMN remark text;   -- 需要 ACCESS EXCLUSIVE,被 A 挡住

即使 A 只更新了一行,它持有的 ROW EXCLUSIVE 表锁也会让 B 的 ACCESS EXCLUSIVE 一直排队。这里的关键认知是:

text
事务持续时间  =  锁的持有时间
锁的持有时间  ≠  语句执行时间

所以「长事务」不只是 MVCC 膨胀问题(见 VACUUM 与膨胀),它同时是锁冲突的主要来源。

要确认锁到底到什么时候释放,可以查等待者与阻塞者的时间:

sql
SELECT
  l.pid,
  l.mode,
  l.granted,
  l.waitstart,
  a.xact_start,
  now() - a.xact_start AS xact_age
FROM pg_locks l
JOIN pg_stat_activity a ON a.pid = l.pid
WHERE l.locktype = 'relation' AND l.relation = 'orders'::regclass;
text
  pid  |        mode         | granted |          waitstart          |          xact_start           |   xact_age
-------+---------------------+---------+-----------------------------+-------------------------------+--------------
 10231 | AccessShareLock     | t       |                             | 2026-09-28 10:12:03.221+08    | 00:03:12.44
 10288 | AccessExclusiveLock | f       | 2026-09-28 10:15:01.876+08   | 2026-09-28 10:15:01.870+08    | 00:00:14.03
(2 rows)

waitstart(PostgreSQL 14+)直接给出「等待开始时刻」,配合 now() 就能算出已经等了多久。

DML 对表的锁与行锁的关系 ​

回顾一个完整的 UPDATE 流程,看两种锁如何协作:

text
1. 解析语句:UPDATE orders SET amount = 1 WHERE id = 1
2. 【表锁】在 orders 上请求 ROW EXCLUSIVE
      ├── 成功 → 继续
      └── 失败(有 SHARE/ACCESS EXCLUSIVE 持有者)→ 等待
3. 【行锁】定位 id = 1 的目标行,在元组头 xmax 上打标记
      ├── 成功 → 继续
      └── 失败(该行被其他事务锁定)→ 等待
4. 写新版本元组、写 WAL
5. 事务结束时统一释放【行锁】与【表锁】

所以一次写操作可能卡在两个地方:卡在表锁(有人在做 DDL/建索引)或卡在行锁(有人锁了同一行)。排查时必须先看 pg_locks.locktype:

locktype卡在wait_event
relation表级锁Lock / relation
transactionid行级锁(事务 ID 锁)Lock / transactionid
tuple行级锁(元组锁,少见)Lock / tuple
sql
-- 判断等待发生在表锁还是行锁层面
SELECT
  pid,
  wait_event_type,
  wait_event,
  pg_blocking_pids(pid) AS blockers
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;

实战示例 ​

场景:线上突然出现大批查询超时告警,排查后发现是一个 ALTER TABLE 被一个忘了提交的长事务阻塞,进而堵住了后面所有查询。

第 1 步:复现事故现场

sql
-- 会话 A:一个「业务查询」,开了事务但迟迟不提交
BEGIN;
SELECT count(*) FROM orders WHERE status = 'paid';
text
 count
-------
  1200
(1 row)

-- 注意:事务仍未结束,ACCESS SHARE 锁一直被持有
sql
-- 会话 B:发版执行 DDL,请求 ACCESS EXCLUSIVE,被 A 挡住
ALTER TABLE orders ADD COLUMN remark text;
text
-- 无输出,一直等待
sql
-- 会话 C:普通业务查询,也来排队(因为 B 的 ACCESS EXCLUSIVE 占住队头)
SELECT * FROM orders WHERE id = 1;
text
-- 同样无输出——被 B 挡住了

第 2 步:定位阻塞链

sql
SELECT
  a.pid,
  a.query,
  a.state,
  a.wait_event_type,
  a.wait_event,
  pg_blocking_pids(a.pid) AS blockers,
  now() - a.xact_start    AS xact_age
FROM pg_stat_activity a
WHERE cardinality(pg_blocking_pids(a.pid)) > 0
ORDER BY a.pid;
text
  pid  |            query             | state  | wait_event_type | wait_event | blockers |   xact_age
-------+------------------------------+--------+-----------------+------------+----------+--------------
 10288 | ALTER TABLE orders ADD COLUMN | active | Lock            | relation   | {10231}  | 00:08:41.12
 10312 | SELECT * FROM orders WHERE id| active | Lock            | relation   | {10288}  | 00:00:22.07
(2 rows)

第 3 步:确认锁的层级与对象

sql
SELECT
  l.pid,
  l.mode,
  l.granted,
  l.relation::regclass AS relation,
  l.waitstart
FROM pg_locks l
WHERE l.locktype = 'relation' AND l.relation = 'orders'::regclass
ORDER BY l.granted DESC, l.pid;
text
  pid  |         mode          | granted | relation |          waitstart
-------+-----------------------+---------+----------+-----------------------------
 10231 | AccessShareLock       | t       | orders   |
 10288 | AccessExclusiveLock   | f       | orders   | 2026-09-28 10:15:01.876+08
 10312 | AccessShareLock       | f       | orders   | 2026-09-28 10:15:23.115+08
(3 rows)

结论一目了然:10231 持锁不放手(granted=t),10288(DDL)在等它,10312(查询)在等 DDL。

第 4 步:处理

优先终止「罪魁祸首」——那个持有锁的长事务(注意 pg_terminate_backend 会回滚它的事务):

sql
-- 终止持有 ACCESS SHARE 的长事务会话
SELECT pg_terminate_backend(10231);
text
 pg_terminate_backend
----------------------
 t
(1 row)

终止后,10288 立刻拿到锁执行 DDL,10312 随后可继续:

text
ALTER TABLE
text
 id | user_id | amount | status
----+---------+--------+--------
  1 |      10 | 100.00 | paid
(1 row)

第 5 步:事后加固——给 DDL 加锁超时

sql
-- 会话 B 的正确做法:设置锁超时,拿不到锁就快速失败,不堵住别人
SET lock_timeout = '3s';
ALTER TABLE orders ADD COLUMN remark text;
text
ERROR:  canceling statement due to lock timeout

快速失败远好于「排队堵死全库」——应用层可以重试,而阻塞链一旦形成,恢复代价极高。

sql
RESET lock_timeout;

清理演示数据:

sql
DROP TABLE orders;

注意事项 ​

  • LOCK TABLE 必须在事务块内使用,否则锁会立即释放,等于没加。
sql
-- 错误!事务外的 LOCK TABLE 毫无作用
LOCK TABLE orders;
  • 不写 IN ... MODE 时默认是 ACCESS EXCLUSIVE,这是最强的锁,很容易误伤。
sql
-- 危险!这等价于 ACCESS EXCLUSIVE,会阻塞所有读写
LOCK TABLE orders;
  • ALTER TABLE 阻塞一切,且会「堵门」。它排在长查询后面时,后续所有查询(哪怕是 SELECT)也会被一起堵住。
sql
-- 危险做法:大表上直接执行,不设超时
ALTER TABLE big_table ADD COLUMN remark text;
  • CREATE INDEX(不带 CONCURRENTLY)会阻塞写入。线上大表建索引必须用 CONCURRENTLY。
sql
-- 不推荐:阻塞写入
CREATE INDEX idx_big ON big_table (col);
  • VACUUM FULL 加 ACCESS EXCLUSIVE 且会重写整表,绝不要在业务高峰执行。
sql
-- 危险!独占+重写,大表上可能数小时
VACUUM FULL orders;
  • 锁持有到事务结束,不是语句结束。 一个忘了提交的事务可以让整张表无法被 DDL 访问。

  • SELECT 只加 ACCESS SHARE,但两个 SELECT 永远不会互相阻塞——这是「读读不互斥」的根本原因。

  • pg_locks.relation 是 OID,直接看会得到数字。用 ::regclass 或 JOIN pg_class 转成表名。

  • pg_stat_activity 里 wait_event_type = 'Lock' 才说明在等锁;LWLock 是另一回事(见 页级锁与死锁),不要混淆。

  • REINDEX 不带 CONCURRENTLY 会加 ACCESS EXCLUSIVE,大表重建索引请用 REINDEX INDEX CONCURRENTLY。

sql
-- 推荐:并发重建
REINDEX INDEX CONCURRENTLY idx_big;

最佳实践 ​

  • 任何线上 DDL 都必须先设 lock_timeout,让冲突快速失败而不是排队堵门。
sql
SET lock_timeout = '3s';
ALTER TABLE orders ADD COLUMN remark text;
RESET lock_timeout;
  • 大表加索引用 CREATE INDEX CONCURRENTLY,虽慢但不阻塞读写。
sql
CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);
  • 尽量缩短事务,把 DDL 与业务 DML 分开,不要让一个事务跨越人工操作。
sql
BEGIN;
-- 只做必要操作,立刻提交
UPDATE orders SET status = 'paid' WHERE id = 100;
COMMIT;
  • 需要多个对象保持顺序时,用一次 LOCK TABLE 批量加锁,避免交错顺序造成死锁。
sql
BEGIN;
LOCK TABLE orders, order_items IN ROW EXCLUSIVE MODE;
-- 后续操作顺序固定
COMMIT;
  • 需要「探路」时用 NOWAIT 快速失败,不要傻等。
sql
BEGIN;
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE NOWAIT;
COMMIT;
  • 监控长事务与等待中的锁,把它做成固定巡检项:
sql
SELECT pid, usename, now() - xact_start AS age, state, left(query, 60) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
  AND now() - xact_start > interval '5 minutes'
ORDER BY age DESC;
  • 给 idle_in_transaction_session_timeout 设一个合理值,自动清理「开了事务不干活」的会话。
sql
ALTER SYSTEM SET idle_in_transaction_session_timeout = '10min';
  • 避免在业务高峰期做 VACUUM FULL / CLUSTER / REINDEX,这些都会加 ACCESS EXCLUSIVE。

  • 排查锁问题时,顺序固定:先 pg_stat_activity 看谁在等(wait_event_type='Lock')→ 再 pg_blocking_pids() 找阻塞者 → 再 pg_locks 看锁模式与对象 → 最后决定「杀谁」或「加什么超时」。

参考链接 ​