在PostgreSQL的并发控制体系里,行锁和表锁分别服务于不同粒度的数据保护需求。行锁通常在进行UPDATE、DELETE或者SELECT ... FOR UPDATE时由数据库自动在目标行上施加,用来阻止其他事务同时修改同一行。表锁的覆盖范围则更大,像ALTER TABLE、TRUNCATE、VACUUM FULL这类操作都会申请不同模式的表级锁。当同一个数据库实例中既有事务持有行锁、又有会话尝试获取表锁时,两种锁的共存关系就变得非常微妙,很多线上故障正是源于对这种共存机制的误判。

行锁与表锁的底层获取机制
PostgreSQL中的锁分为表级和行级两个层次,但行锁在内部实现上并不会单独占用一个全局锁结构,而是借助行的元组头信息中的标识位以及多版本并发控制来判断可见性。当一个事务执行UPDATE某行时,它会在该行上标记一个事务ID,其他试图修改的事务会感知到冲突并进入等待。表锁则通过pg_locks系统视图中relation类型的记录来体现,其锁模式包括ACCESS SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、ACCESS EXCLUSIVE等。
需要特别注意的是,执行DML语句本身也会获取表级锁,只是模式较弱。例如UPDATE会获取ROW EXCLUSIVE表锁,这和真正修改表结构的ACCESS EXCLUSIVE并不等价。当另一个会话发起ALTER TABLE时,它申请的ACCESS EXCLUSIVE表锁会和已有的ROW EXCLUSIVE冲突,同时也会和那些尚未提交事务持有的行锁产生间接阻塞,因为行锁依赖所属表不能被结构变更。
我们可以通过一段简单的SQL观察这种共存状态。下面的代码展示了如何模拟一个行锁并查看锁视图:
-- 会话一:开启事务并更新一行,产生行锁与ROW EXCLUSIVE表锁 BEGIN; UPDATE orders SET status = 'paid' WHERE id = 1001; -- 会话二:查看当前锁情况 SELECT pid, locktype, relation::regclass, mode, granted FROM pg_locks WHERE relation = 'orders'::regclass;
从查询结果中可以看到,locktype为relation的记录表示表锁,而行锁在pg_locks里通常以transactionid或者tuple形式关联。理解这种双层结构,是分析共存场景的基础。
共存场景下的典型阻塞与死锁案例
最常见的问题出现在后台任务对大表做DDL操作时。假设业务高峰期有多个事务正在对orders表的不同行做更新,此时运维脚本执行ALTER TABLE orders ADD COLUMN remark text。该语句需要SHARE UPDATE EXCLUSIVE或者更高模式的表锁,它必须等待所有已持有ROW EXCLUSIVE的事务结束。由于这些事务可能还持有行锁,DDL被阻塞,而后续新的DML又因为排队等待DDL释放资源,从而形成雪崩式等待。
另一种隐蔽情况是显式表锁与行锁交织。有些开发者在批量处理前写LOCK TABLE orders IN ACCESS EXCLUSIVE MODE,意图独占表。如果此时别的事务刚对某行加了FOR UPDATE行锁且未提交,LOCK TABLE会卡住;而卡住期间,原本的行锁持有者若再尝试访问被锁表的其他资源,就可能演变为死锁,数据库检测到后只能回滚其中之一。
以下示例演示了如何安全地以非阻塞方式尝试获取表锁,避免长等待拖垮业务:
-- 尝试以nowait方式加表锁,若已被行锁或其他锁占用则立刻报错而非等待 BEGIN; LOCK TABLE orders IN ACCESS EXCLUSIVE MODE NOWAIT; -- 成功则继续DDL,失败则在应用中捕获异常并重试或跳过 ALTER TABLE orders ADD COLUMN remark text; COMMIT;
借助NOWAIT选项,应用可以把不可控的长时间阻塞转化成可处理的异常逻辑,从而保护核心链路。
设计层面的规避与运维排查手段
在系统设计时,应当尽量把DDL操作和繁忙事务高峰错开,采用在线DDL工具或者在低峰期执行。对于必须频繁变更结构的场景,可以考虑将易变字段放到扩展表或使用JSON类型减少真实DDL次数。同时,把长事务拆短,让行锁尽早释放,是从源头降低表锁等待概率的根本办法。
运维侧则要养成监控pg_stat_activity与pg_locks的习惯。通过关联pid和wait_event类型,可以快速定位是行锁等待还是表锁等待。如下查询能列出当前被阻塞的会话及其阻塞源头:
SELECT a.pid AS blocked_pid,
a.query AS blocked_query,
b.pid AS blocking_pid,
b.query AS blocking_query
FROM pg_stat_activity a
JOIN pg_locks l1 ON a.pid = l1.pid AND NOT l1.granted
JOIN pg_locks l2 ON l1.relation = l2.relation AND l2.granted
JOIN pg_stat_activity b ON l2.pid = b.pid
WHERE a.datname = current_database();
拿到阻塞链后,如果是表锁被长事务行锁阻塞,可以评估杀掉闲置长事务;若是死锁风险,则应调整代码里的加锁顺序。总之,只有在理解行锁与表锁共存规则后,才能把PostgreSQL的并发能力发挥到稳定可靠的水平。
PostgreSQL行锁表锁修改时间:2026-08-13 11:09:34