导读:本期聚焦于小伙伴创作的《PostgreSQL行锁与表锁共存场景会带来什么问题以及如何正确处理?》,敬请观看详情。在高频交易系统中,一张订单表同时被多个服务修改单行记录又周期性做结构变更,往往引发难以排查的阻塞。行锁由UPDATE或SELECT FOR UPDATE在指定行上产生,表锁则在ALTER TABLE或某些DDL时施加,二者共存时并非简单叠加。若表锁请求模式与已有行锁冲突,后续会话会排队甚至级联等待,导致接口超时。理解LockType与锁冲突矩阵,结合nowait与超时控制,才能在设计层规避死锁与长事务堆积。

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

PostgreSQL行锁与表锁共存场景会带来什么问题以及如何正确处理?

行锁与表锁的底层获取机制

PostgreSQL中的锁分为表级和行级两个层次,但行锁在内部实现上并不会单独占用一个全局锁结构,而是借助行的元组头信息中的标识位以及多版本并发控制来判断可见性。当一个事务执行UPDATE某行时,它会在该行上标记一个事务ID,其他试图修改的事务会感知到冲突并进入等待。表锁则通过pg_locks系统视图中relation类型的记录来体现,其锁模式包括ACCESS SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVEACCESS 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;

从查询结果中可以看到,locktyperelation的记录表示表锁,而行锁在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_activitypg_locks的习惯。通过关联pidwait_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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。