表级锁(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 能不能并存」这个更宏观的问题。
举个最小的例子:
SELECT * FROM orders WHERE id = 1;这条语句不会锁住任何一行,但它必然在 orders 表上持有一把 ACCESS SHARE 锁——因为「读表」这件事必须先声明「我只是看看,不让别人把表结构改掉」。
八种锁模式完整清单
PostgreSQL 有八种表级锁模式,按「锁定强度」从弱到强排:
| 锁模式 | 英文全称 | 语义 | 冲突强度 |
|---|---|---|---|
ACCESS SHARE | Access Share Lock | 只读表,最弱的锁 | 只与 ACCESS EXCLUSIVE 冲突 |
ROW SHARE | Row Share Lock | 打算从表里「选中并锁定」若干行 | 与 EXCLUSIVE / ACCESS EXCLUSIVE 冲突 |
ROW EXCLUSIVE | Row Exclusive Lock | 打算修改表里的行(写表) | 与 SHARE/SHARE ROW EXCLUSIVE/EXCLUSIVE/ACCESS EXCLUSIVE 冲突 |
SHARE UPDATE EXCLUSIVE | Share Update Exclusive Lock | 表维护类操作(VACUUM/建索引) | 自冲突,且与 SHARE 及以上冲突 |
SHARE | Share Lock | 只读且禁止并发写(如建索引扫描) | 与写表类锁冲突 |
SHARE ROW EXCLUSIVE | Share Row Exclusive Lock | 比 SHARE 更严,自冲突 | 自冲突,与写表类锁冲突 |
EXCLUSIVE | Exclusive Lock | 最高级的行锁类,只允许 ACCESS SHARE | 仅与 ACCESS SHARE 兼容 |
ACCESS EXCLUSIVE | Access 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 表锁,成功之后才去逐行加行级锁。如果表锁就被别人挡住了,行锁根本不会开始。
锁的生命周期:持有到事务结束
这是初学者最容易误解的一点:表级锁(以及行级锁)默认持有到整个事务结束,而不是单条语句结束。
BEGIN; -- 事务开始
UPDATE orders SET status='x' -- 在 orders 上拿 ROW EXCLUSIVE,直到 COMMIT 才释放
WHERE id = 1;
-- ... 中间可以干很多别的事 ...
COMMIT; -- 此刻才释放所有锁在 COMMIT 之前的任何时刻,另一个会话的 ALTER TABLE orders ... 都会一直等待。这就是「一个忘了提交的事务也能锁死整张表」的原因。
语法与用法
显式加表锁用 LOCK TABLE:
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 表名;最简可运行示例:
-- 显式加一把 SHARE 锁,阻止其他会话写入
BEGIN;
LOCK TABLE orders IN SHARE MODE;LOCK TABLE不指定模式时默认是 ACCESS EXCLUSIVE:
BEGIN;
LOCK TABLE orders; -- 等价于 LOCK TABLE orders IN ACCESS EXCLUSIVE MODE;LOCK TABLE查看当前会话持有的表锁:
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(); relation | mode | granted
----------+------------------+---------
orders | AccessShareLock | t
orders | ShareLock | t
(2 rows)同一个会话可以对同一张表持有多把不同模式的锁(PostgreSQL 会把它们合并为最强的那把对外体现),上例中该会话最终对外表现为 ShareLock。
详细说明
八种锁模式逐个详解
ACCESS SHARE(最弱):任何 SELECT(不含 FOR UPDATE 系列)都会加它。它的唯一目的是阻止 ACCESS EXCLUSIVE——即「我在读,你别改表结构」。多个 ACCESS SHARE 之间完全不冲突,所以两个 SELECT 永远不会互相阻塞。
-- 只读查询:自动加 ACCESS SHARE,可无限并发
SELECT count(*) FROM orders; count
-------
2000
(1 row)ROW SHARE:SELECT ... FOR UPDATE / FOR NO KEY UPDATE / FOR SHARE / FOR KEY SHARE 会加它。表达的是「我打算锁住部分行」,因此它不与写表的 ROW EXCLUSIVE 冲突(大家都是改行),只与 EXCLUSIVE / ACCESS EXCLUSIVE 冲突。
-- 行锁系列语句:自动加 ROW SHARE 表锁
SELECT * FROM orders WHERE id = 1 FOR UPDATE;ROW EXCLUSIVE:INSERT / UPDATE / DELETE / MERGE 会加它。它是最常见的写锁,与 SHARE 系列都不兼容——这解释了「建索引时写操作会被挡住」。
-- 写语句:自动加 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(写表)兼容,所以在线建索引时业务写入照常进行。
-- 在线建索引:加 SHARE UPDATE EXCLUSIVE,不阻塞 DML
CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);SHARE:CREATE INDEX(不带 CONCURRENTLY)会加它。它阻止并发写入(ROW EXCLUSIVE),但允许并发读(ACCESS SHARE)。这就是「普通建索引不阻塞查询,但阻塞写入」的原因。
-- 普通建索引:加 SHARE,阻塞写入但不阻塞读
CREATE INDEX idx_orders_no ON orders (order_no);SHARE ROW EXCLUSIVE:CREATE TRIGGER 以及部分 ALTER TABLE 形式会加它。它比 SHARE 更严——自冲突,且与写表类锁冲突。它只允许 ACCESS SHARE 并发。
-- 建触发器:加 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(只读)并发,所有写操作都要等待。这个锁很少见,但一旦出现,说明有并发的物化视图刷新在进行。
-- 并发刷新物化视图:加 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 等。
-- 最强锁:此刻表上任何其他访问(哪怕 SELECT)都要排队
ALTER TABLE orders ADD COLUMN remark text;ALTER TABLEACCESS EXCLUSIVE 是「排队堵门」的元凶
ALTER TABLE 排队时,它不仅挡住后来的写操作,还会挡住后来的读操作——因为它们都要拿 ACCESS SHARE,而 ACCESS EXCLUSIVE 与 ACCESS SHARE 冲突。所以一个 ALTER TABLE 排在一个长查询后面时,它会把后面所有查询也一起堵住,形成雪崩。
各条 SQL 自动加什么锁(获取场景速查)
| SQL | 自动表锁 | 是否阻塞写 | 是否阻塞读 |
|---|---|---|---|
SELECT | ACCESS SHARE | 否 | 否 |
SELECT ... FOR UPDATE / FOR SHARE 等 | ROW SHARE | 否 | 否 |
INSERT / UPDATE / DELETE / MERGE | ROW EXCLUSIVE | 否 | 否 |
VACUUM(无 FULL) | SHARE UPDATE EXCLUSIVE | 否 | 否 |
ANALYZE | SHARE UPDATE EXCLUSIVE | 否 | 否 |
CREATE INDEX CONCURRENTLY | SHARE UPDATE EXCLUSIVE | 否 | 否 |
CREATE STATISTICS | SHARE UPDATE EXCLUSIVE | 否 | 否 |
REINDEX CONCURRENTLY | SHARE UPDATE EXCLUSIVE | 否 | 否 |
CREATE INDEX(普通) | SHARE | 是 | 否 |
CREATE TRIGGER | SHARE ROW EXCLUSIVE | 是 | 否 |
REFRESH MATERIALIZED VIEW CONCURRENTLY | EXCLUSIVE | 是 | 否 |
ALTER TABLE(多数形式) | ACCESS EXCLUSIVE | 是 | 是 |
DROP TABLE / DROP INDEX | ACCESS EXCLUSIVE | 是 | 是 |
TRUNCATE | ACCESS EXCLUSIVE | 是 | 是 |
VACUUM FULL | ACCESS EXCLUSIVE | 是 | 是 |
CLUSTER | ACCESS EXCLUSIVE | 是 | 是 |
REINDEX(非并发) | ACCESS EXCLUSIVE | 是 | 是 |
LOCK TABLE(默认) | ACCESS EXCLUSIVE | 是 | 是 |
这张表是排查锁事故的第一对照表:看到「查询被阻塞了」,先想「当前有没有上面带 是 的操作在跑」。
锁冲突矩阵(8 × 8)
下表是官方冲突矩阵:行 = 请求的锁模式,列 = 已被持有的锁模式,X 表示冲突(请求方要等待)。
| 请求 \ 已持有 | ACCESS SHARE | ROW SHARE | ROW EXCL | SHARE UPDATE EXCL | SHARE | SHARE ROW EXCL | EXCL | ACCESS EXCL |
|---|---|---|---|---|---|---|---|---|
| ACCESS SHARE | X | |||||||
| ROW SHARE | X | X | ||||||
| ROW EXCLUSIVE | X | X | X | |||||
| SHARE UPDATE EXCLUSIVE | X | X | X | X | X | |||
| SHARE | X | X | X | X | ||||
| SHARE ROW EXCLUSIVE | X | X | X | X | X | |||
| EXCLUSIVE | X | X | X | X | X | X | ||
| ACCESS EXCLUSIVE | X | X | X | X | X | X | X | X |
读法示例:
- 第 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 的典型用途是「在事务内一次性锁定一批表,保证后续操作的锁顺序一致」,从而避免死锁。
-- 在事务内先锁两张表,避免后续交错加锁造成死锁
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 让锁请求立即失败而不是等待,适合「拿不到就不做」的场景:
-- 拿不到锁立即报错,不排队
BEGIN;
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE NOWAIT;ERROR: could not obtain lock on relation "orders"不写 IN ... MODE 就是 ACCESS EXCLUSIVE,且 LOCK TABLE 必须在事务块内(否则等价于给单条语句加锁后立即释放,没有意义):
-- 错误!事务外 LOCK TABLE 没有意义(自动提交后立即释放)
LOCK TABLE orders;WARNING: there is no transaction in progress
LOCK TABLELOCK TABLE ... IN ACCESS SHARE MODE(比默认弱)一般用于「占位」:拿到这把锁意味着当前没有 ACCESS EXCLUSIVE 持有者,可用于探测表是否正被 DDL 操作。
-- 用 ACCESS SHARE NOWAIT 探测是否有 DDL 在跑
BEGIN;
LOCK TABLE orders IN ACCESS SHARE MODE NOWAIT;
COMMIT;ACCESS EXCLUSIVE 与 ACCESS SHARE 的阻塞关系
这是本节最需要理解透的一组关系:
ACCESS EXCLUSIVE ──冲突── ACCESS SHARE
▲ ▲
│ │
ALTER TABLE / DROP 任意 SELECT为什么 DDL 会阻塞查询?因为 DDL 要改的是表的结构(比如加列),而正在跑的 SELECT 可能已经基于旧结构生成了执行计划。如果不阻塞,就会出现「查询读到一半表结构变了」的不一致。所以 PostgreSQL 选择让 ACCESS EXCLUSIVE 与 ACCESS SHARE 冲突。
-- 会话 A:一个跑得很久的查询,持有 ACCESS SHARE
SELECT pg_sleep(600), count(*) FROM orders;
-- 会话 B:DDL 需要 ACCESS EXCLUSIVE,被 A 挡住
ALTER TABLE orders ADD COLUMN remark 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 FULL | ACCESS EXCLUSIVE | 阻塞一切,且会重写整表 |
CREATE INDEX CONCURRENTLY | SHARE UPDATE EXCLUSIVE | 不阻塞读写,但耗时长、可能失败 |
CREATE INDEX | SHARE | 阻塞写,不阻塞读 |
ALTER TABLE ... ADD COLUMN(无默认值) | ACCESS EXCLUSIVE | 阻塞一切(但很快) |
ALTER TABLE ... ADD COLUMN DEFAULT x(PG 11+) | ACCESS EXCLUSIVE | 阻塞一切,但不再重写整表 |
ALTER TABLE ... VALIDATE CONSTRAINT | SHARE UPDATE EXCLUSIVE | 不阻塞读写 |
-- VACUUM FULL 会重写整表,必须独占
VACUUM FULL orders;VACUUMPostgreSQL 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 才能看到表名:
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; 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 更简洁:
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 数组:
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; 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)。
用一条查询完整还原「等待链」:
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; 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 告诉你阻塞者的事务开了多久——这个值很大时通常意味着有人忘了提交。
表锁与事务边界:锁持有到事务结束
用可视化的方式看锁的持有窗口:
-- 会话 A
BEGIN;
UPDATE orders SET amount = 0 WHERE id = 1; -- 拿到 ROW EXCLUSIVE,持有中
-- 此处不 COMMIT-- 会话 B:立刻执行,会一直等待
ALTER TABLE orders ADD COLUMN remark text; -- 需要 ACCESS EXCLUSIVE,被 A 挡住即使 A 只更新了一行,它持有的 ROW EXCLUSIVE 表锁也会让 B 的 ACCESS EXCLUSIVE 一直排队。这里的关键认知是:
事务持续时间 = 锁的持有时间
锁的持有时间 ≠ 语句执行时间所以「长事务」不只是 MVCC 膨胀问题(见 VACUUM 与膨胀),它同时是锁冲突的主要来源。
要确认锁到底到什么时候释放,可以查等待者与阻塞者的时间:
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; 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 流程,看两种锁如何协作:
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 |
-- 判断等待发生在表锁还是行锁层面
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 步:复现事故现场
-- 会话 A:一个「业务查询」,开了事务但迟迟不提交
BEGIN;
SELECT count(*) FROM orders WHERE status = 'paid'; count
-------
1200
(1 row)
-- 注意:事务仍未结束,ACCESS SHARE 锁一直被持有-- 会话 B:发版执行 DDL,请求 ACCESS EXCLUSIVE,被 A 挡住
ALTER TABLE orders ADD COLUMN remark text;-- 无输出,一直等待-- 会话 C:普通业务查询,也来排队(因为 B 的 ACCESS EXCLUSIVE 占住队头)
SELECT * FROM orders WHERE id = 1;-- 同样无输出——被 B 挡住了第 2 步:定位阻塞链
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; 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 步:确认锁的层级与对象
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; 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 会回滚它的事务):
-- 终止持有 ACCESS SHARE 的长事务会话
SELECT pg_terminate_backend(10231); pg_terminate_backend
----------------------
t
(1 row)终止后,10288 立刻拿到锁执行 DDL,10312 随后可继续:
ALTER TABLE id | user_id | amount | status
----+---------+--------+--------
1 | 10 | 100.00 | paid
(1 row)第 5 步:事后加固——给 DDL 加锁超时
-- 会话 B 的正确做法:设置锁超时,拿不到锁就快速失败,不堵住别人
SET lock_timeout = '3s';
ALTER TABLE orders ADD COLUMN remark text;ERROR: canceling statement due to lock timeout快速失败远好于「排队堵死全库」——应用层可以重试,而阻塞链一旦形成,恢复代价极高。
RESET lock_timeout;清理演示数据:
DROP TABLE orders;注意事项
LOCK TABLE必须在事务块内使用,否则锁会立即释放,等于没加。
-- 错误!事务外的 LOCK TABLE 毫无作用
LOCK TABLE orders;- 不写
IN ... MODE时默认是ACCESS EXCLUSIVE,这是最强的锁,很容易误伤。
-- 危险!这等价于 ACCESS EXCLUSIVE,会阻塞所有读写
LOCK TABLE orders;ALTER TABLE阻塞一切,且会「堵门」。它排在长查询后面时,后续所有查询(哪怕是SELECT)也会被一起堵住。
-- 危险做法:大表上直接执行,不设超时
ALTER TABLE big_table ADD COLUMN remark text;CREATE INDEX(不带CONCURRENTLY)会阻塞写入。线上大表建索引必须用CONCURRENTLY。
-- 不推荐:阻塞写入
CREATE INDEX idx_big ON big_table (col);VACUUM FULL加ACCESS EXCLUSIVE且会重写整表,绝不要在业务高峰执行。
-- 危险!独占+重写,大表上可能数小时
VACUUM FULL orders;锁持有到事务结束,不是语句结束。 一个忘了提交的事务可以让整张表无法被 DDL 访问。
SELECT只加ACCESS SHARE,但两个SELECT永远不会互相阻塞——这是「读读不互斥」的根本原因。pg_locks.relation是 OID,直接看会得到数字。用::regclass或 JOINpg_class转成表名。pg_stat_activity里wait_event_type = 'Lock'才说明在等锁;LWLock是另一回事(见 页级锁与死锁),不要混淆。REINDEX不带CONCURRENTLY会加ACCESS EXCLUSIVE,大表重建索引请用REINDEX INDEX CONCURRENTLY。
-- 推荐:并发重建
REINDEX INDEX CONCURRENTLY idx_big;最佳实践
- 任何线上 DDL 都必须先设
lock_timeout,让冲突快速失败而不是排队堵门。
SET lock_timeout = '3s';
ALTER TABLE orders ADD COLUMN remark text;
RESET lock_timeout;- 大表加索引用
CREATE INDEX CONCURRENTLY,虽慢但不阻塞读写。
CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);- 尽量缩短事务,把 DDL 与业务 DML 分开,不要让一个事务跨越人工操作。
BEGIN;
-- 只做必要操作,立刻提交
UPDATE orders SET status = 'paid' WHERE id = 100;
COMMIT;- 需要多个对象保持顺序时,用一次
LOCK TABLE批量加锁,避免交错顺序造成死锁。
BEGIN;
LOCK TABLE orders, order_items IN ROW EXCLUSIVE MODE;
-- 后续操作顺序固定
COMMIT;- 需要「探路」时用
NOWAIT快速失败,不要傻等。
BEGIN;
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE NOWAIT;
COMMIT;- 监控长事务与等待中的锁,把它做成固定巡检项:
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设一个合理值,自动清理「开了事务不干活」的会话。
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看锁模式与对象 → 最后决定「杀谁」或「加什么超时」。
下一节