内容提要
PostgreSQL 行锁不存于共享内存,而是写入元组头:t_xmax 记录加锁事务 ID,t_infomask 位标记锁类型。四种锁强度由三位编码,FOR UPDATE 与键更新、删除冲突。多事务同时加锁时用 MultiXactId 指向磁盘成员列表,外键检查自动加 FOR KEY SHARE。锁提交后标记仍留在页上,直到 VACUUM 清理。大量短事务与长事务并发会快速消耗 multixact 成员空间,需监控避免超限。
延伸解读
行锁的存储机制与可见性
PostgreSQL 将行锁信息直接写入元组头部,而非共享内存锁表。t_xmax 记录加锁事务 ID,t_infomask 中的位标记锁类型。这种设计避免了锁表容量限制,但每次加锁都变成对页面的写操作。读者通过检查 HEAP_XMAX_LOCK_ONLY 等标志,仍能看到行数据,无需等待锁释放。锁提交后,标记仍保留在页上,直到 VACUUM 清理,这解释了为何锁释放后页面内容不变。
锁模式与冲突关系
四种行锁强度由三个位编码:FOR KEY SHARE、FOR SHARE、FOR NO KEY UPDATE、FOR UPDATE。其中 FOR UPDATE 与所有模式冲突,而 FOR NO KEY UPDATE 与 FOR KEY SHARE 兼容。外键检查自动加 FOR KEY SHARE,因此更新非键列(如余额)不会阻塞子表插入,但删除或更新键列会。应用应避免默认使用 FOR UPDATE,改用 FOR NO KEY UPDATE 可减少不必要的阻塞。
MultiXact 的磁盘开销与监控
当多个事务同时锁定同一行时,t_xmax 存储 MultiXactId,指向磁盘上的成员列表。大量短事务与长事务并发会快速消耗 multixact 成员空间,每个成员约 5 字节,上限约 20GB。监控 next_multi_offset 比 next_multixact_id 更重要。达到阈值会触发 autovacuum 冻结,即使表无死元组。VACUUM 可清理旧 multixact,但文件截断需所有数据库的 datminmxid 推进。
Q&A
PostgreSQL 的行锁具体存储在哪里?
行锁不存储在共享内存中,而是直接写入元组头。具体来说,加锁事务的 ID 被记录在 t_xmax 字段中,锁的类型则通过 t_infomask 和 t_infomask2 中的标志位来标记。
PostgreSQL 的四种行锁强度分别对应哪些位模式?
四种行锁强度由三个位编码:FOR KEY SHARE 为 100,FOR SHARE 为 110,FOR NO KEY UPDATE 为 010,FOR UPDATE 为 011。这些位分布在 t_infomask 和 t_infomask2 中。
为什么事务提交后行锁标记仍然留在页上?
因为 COMMIT 不会重新访问事务锁定过的页面,否则需要记住每一个页面。锁的释放是通过检查事务状态来确定的,而元组头上的锁标记会一直保留,直到 VACUUM 清理时才会被清除或设置提示位。
MultiXactId 是什么?它在什么情况下会被使用?
MultiXactId 是一个 32 位的标识符,当多个事务同时在同一行上持有锁时,t_xmax 字段会存储一个 MultiXactId,而不是单个事务 ID。它指向存储在磁盘上的成员列表,该列表记录了每个锁持有者的事务 ID 和锁模式。
外键检查会加什么类型的行锁?
外键检查会自动在父表行上加 FOR KEY SHARE 锁。这种锁与 FOR NO KEY UPDATE 兼容,因此可以在子表插入的同时更新父表的非键列。
大量短事务与长事务并发时,为什么需要监控 multixact 成员空间?
因为每个短事务插入子行时都会形成新的 multixact 成员,而长事务持有键共享锁会与这些短事务组合成新的 multixact。这会导致 multixact 成员数量快速增长,可能耗尽 2^32 个成员槽位,从而触发 'multixact "members" limit exceeded' 错误。因此需要监控 next_multi_offset 等指标。