在PostgreSQL中排查锁等待时,经常会先查询pg_locks视图,但该视图侧重对象级锁和事务号锁,难以直接告诉你是哪一行被什么事务锁定。当业务出现两个事务同时更新同一条记录导致卡住时,pgrowlocks扩展可以从物理行角度给出答案。它读取表的堆存储结构,逐行返回当前可见的行锁信息,帮助定位锁竞争的具体位置。

行级锁在PostgreSQL中不会像表级锁那样完整地记录在共享内存的锁管理器中,而是以状态标记的形式写入每一行元组的头部。pg_locks虽然能看到一些tuple锁和transactionid锁,但无法映射到具体的行TID。pgrowlocks正是为了弥补这个盲区而设计的官方扩展模块。
如何安装pgrowlocks扩展
pgrowlocks属于PostgreSQL源码自带的contrib扩展,如果你的环境中安装了完整发行包,通常可以直接创建,无需额外下载。创建命令非常简单:
CREATE EXTENSION pgrowlocks;
执行后会增加一个名为pgrowlocks的函数,函数签名可以理解为pgrowlocks(relname text) returns setof record。使用时需要传入目标表的名称,例如SELECT * FROM pgrowlocks('inventory');。这里的表名是普通字符串参数,因此必须注意表达式中的单引号用法。函数返回一组记录,每条记录对应堆文件中被行锁标记影响的一行。
删除扩展也很容易,直接执行DROP EXTENSION pgrowlocks;即可。扩展中的C函数会访问表的物理存储,因此要求调用者对目标表有读取权限,否则会因权限不足而报错。通常普通业务账号拥有表的SELECT权限时就可以正常查询。
如果不想在数据库中创建扩展,也可以只调用函数一次试试,但要明白pgrowlocks不是内置视图,必须先创建扩展才能使用。这个函数的好处是不需要修改表结构,也不会留下后台常驻进程,只在被调用时进行一次扫描。
解读pgrowlocks返回字段
先创建一个测试表并插入两行数据,然后在一个事务中对其中一行加锁。
CREATE TABLE inventory ( item_id integer PRIMARY KEY, quantity integer NOT NULL ); INSERT INTO inventory VALUES (1, 100), (2, 200);
在会话一中开启事务并锁定一行:
BEGIN; SELECT * FROM inventory WHERE item_id = 1 FOR UPDATE;
这时保持事务不提交,然后在同一会话或另一个会话中查询pgrowlocks:
SELECT * FROM pgrowlocks('inventory');
返回结果可能类似下面这样:
locked_row | locker | multi | xids | modes | pids
------------+--------+-------+-----------+-------------------+-------
(0,1) | 1234 | f | {1234} | {"For Update"} | {5678}
其中locked_row是元组TID,括号中的两个数字分别表示块号和块内偏移。locker是当前持有行锁的事务号。multi为假时表示这是普通单一事务锁,xids数组里只有一个事务号。modes显示锁模式,这里是For Update;如果使用SELECT FOR SHARE,模式会显示为For Share。pids是持有该行锁的后端进程号,方便关联pg_stat_activity查看具体会话。
如果同一行被多个事务以共享模式锁定,或者既有共享锁又有更新锁,PostgreSQL会使用MultiXact机制管理多个事务对同一行的锁状态。此时multi值为真,xids、modes、pids都会是数组,数组中的元素按照多事务成员顺序展开。例如两个会话都对同一行执行FOR SHARE,查询pgrowlocks时就能看到这一行同时被两个事务持有共享锁,这样就不会误判为只有一个持锁者。
pg_locks视图记录的是锁管理器中的锁,行级锁并不在其中。因此想确认行锁细节时,pgrowlocks比pg_locks更直接。不过需要注意,pgrowlocks展示的是当前已经持有的行锁,如果某个事务正在等待获取同一行的更新锁,但还没有真正获得锁,那么它在pgrowlocks中不会以等待者身份出现,这类信息需要结合pg_stat_activity的等待事件和pg_locks中的事务锁来判断。
用pgrowlocks定位行锁阻塞
典型场景是应用日志中出现事务卡住,某个更新语句长时间不返回。可以先用以下语句找到等待中的会话:
SELECT pid, state, wait_event_type, wait_event, query FROM pg_stat_activity WHERE state IS NOT NULL AND pid <> pg_backend_pid();
如果发现某个会话的等待事件是transactionid,说明它在等待另一个事务结束。接下来要确定到底等的是哪一行,就需要查询pgrowlocks。假设等待发生在inventory表,可以执行:
SELECT locked_row, locker, xids, modes, pids
FROM pgrowlocks('inventory');
拿到locked_row后,就能用TID直接查询该行内容,例如SELECT * FROM inventory WHERE ctid = '(0,1)';。结合pids数组可以找到正在持有锁的后端进程,再通过pg_stat_activity查看它的事务开始时间、最后执行的语句,判断要不要终止该进程。终止进程可以使用SELECT pg_cancel_backend(pid);或SELECT pg_terminate_backend(pid);,前者更温和,后者更彻底。
在多事务共享锁的场景中,一个行锁可能对应多个后端进程。例如多个只读事务都对同一行执行FOR SHARE,它们彼此不会阻塞,却会阻塞后续试图执行FOR UPDATE的会话。此时pgrowlocks返回的pids数组包含所有共享锁持有者,排查时不能只关注某一个进程,需要结合事务开始时间和应用日志决定是否批量终止。
除了排查阻塞,pgrowlocks还可以用来审计应用程序的事务行为。比如业务代码中无意识地使用了SELECT FOR UPDATE,导致热点行长期被锁定。通过定时采集pgrowlocks结果,可以发现哪些行频繁出现锁,从而反向优化事务设计,缩小锁定范围或改用乐观并发控制。
实现原理与使用注意事项
pgrowlocks的C代码位于PostgreSQL源码的contrib/pgrowlocks/pgrowlocks.c文件中。它的主要逻辑是调用堆扫描接口逐块读取目标表的数据页,对每一行元组检查头部信息。PostgreSQL在元组头中维护了多个锁相关标记位,包括HEAP_XMAX_LOCK_ONLY、HEAP_XMAX_EXCL_LOCK、HEAP_XMAX_SHR_LOCK等。pgrowlocks根据这些标记判断行是否被锁定,以及属于排他锁还是共享锁。
如果锁标记指向MultiXact,pgrowlocks会进一步调用多事务管理函数获取成员事务号和对应的锁模式。这个过程中需要读取多事务段文件,因此查询pgrowlocks涉及的基础操作比普通SQL更底层。函数最终把每一行的TID、锁持有者信息组织成记录返回给客户端。由于它是一次完整表扫描,表越大查询耗时越长。
在生产环境中使用pgrowlocks时要特别注意性能开销。对于几千万行的大表,执行一次SELECT * FROM pgrowlocks('big_table');可能扫描所有数据页并解析每行头信息,可能持续数秒甚至更久。建议在已经锁定问题范围的情况下,只对相关表执行查询,避免在业务高峰期频繁调用。此外,扫描期间可能读到未提交数据,行锁信息与实际事务状态之间可能存在瞬时差异,因此结果应作为诊断线索,而不是严格的锁管理器快照。
从PostgreSQL版本演进来看,pgrowlocks的思路也被一些监控工具借鉴,比如用于展示行锁等待拓扑。对于无法创建扩展的环境,还可以使用pageinspect扩展直接查看页内元组头信息,但操作更加底层,远不如pgrowlocks方便。掌握pgrowlocks的输出字段和查询方法,能够显著提升定位行锁阻塞的效率。
PostgreSQL行锁pgrowlocks锁等待诊断修改时间:2026-08-20 17:21:46