PostgreSQL 教程: 并发锁定和组合事务

六月 18, 2024

摘要:在本教程中,您将了解什么是组合事务,它为什么会存在,以及会在什么情况下出现。

目录

组合事务简介

在 PostgreSQL 中,两个进程(甚至三个进程)可以同时锁定同一行,只要它们的锁不相互冲突。关于各种锁与其他类型的锁相冲突的规则,在文档中有很好的说明。

当您在 PostgreSQL 中锁定一行时,您在做的是,将事务 ID 放在存储中相应元组的xmax字段中。这样,任何来寻找这一行的人都会知道您已经锁定了它。然后,后来者可以等待该元组上的锁:

事务 768 事务 769 说明
SELECT * FROM foo WHERE x = 9 FOR UPDATE; 事务768现在拥有一个行锁。该行的xmax字段包含了值768
SELECT * FROM foo WHERE x = 9 FOR UPDATE; 事务769检索当前行,看到事务768已持有与其想要的锁冲突的锁,并等待事务768结束。

但是,如果两个进程同时都想锁定同一行怎么办?例如:

事务 772 事务 773 说明
SELECT * FROM foo WHERE x = 9 FOR SHARE; 之后,事务772拥有一个行锁。该行的xmax字段包含了值772
SELECT * FROM foo WHERE x = 9 FOR SHARE; 现在会发生什么?

事务773不能简单地将其事务 ID 写入xmax字段。那样相当于抢占了事务772的行锁,这会破坏锁的全部意义。为了解决这个问题,PostgreSQL 创建了一个组合事务。一个组合事务实质上是将一组事务捆绑在一起,以便这些事务可以同时锁定同一行。新的组合事务 ID 将写入到行的xmax字段。

事务 772 事务 773 说明
SELECT * FROM foo WHERE x = 9 FOR SHARE;
SELECT * FROM foo WHERE x = 9 FOR SHARE; 现在,两个事务都锁定了该行。该行的xmax字段被设置为14,这是一个组合事务 ID。组合事务14根据事务 ID 指向事务772773
COMMIT; 事务773现在已结束,但该行的xmax值仍为14。由于组合事务是不可变的,因此组合事务14仍然会指向现已失效的事务773,以及正在进行中的事务772
COMMIT; 两个事务都结束后,在该行上不再有处于活动状态的锁。它的xmax值仍然是14,并且会一直保持为14,直到另一个进程锁定该行,或者该表被进行 VACUUM

值得重申的是,组合事务是不可变的。如果两个事务104108,分别作为组合事务19的一部分,都锁定了 R 行,并且事务117也要锁定该行,则事务117不能简单地联接组合事务19。相反,要创建一个 ID 为20的新的组合事务,其中包含了事务104108117

这意味着,每当一个额外的事务想要锁定一行时,PostgreSQL 都必须将整个新的组合事务写入到缓冲区。对于大型的组合事务,所有这些读取和写入的时间成本可能会变得非常大。特别是因为对基础数据区域的访问,会受到一组全局轻量级锁的约束。

组合事务会不涉及多个事务吗?

在实际情况中,组合事务也可能会在单个事务中出现。假设,我们正在做这样的事情:

BEGIN;

SELECT * FROM queue_jobs
  WHERE id = 4
  FOR SHARE;

SAVEPOINT foo;

SELECT * FROM queue_jobs
  WHERE id = 4
  FOR UPDATE;

从技术上讲,SAVEPOINT 不应该会创建新的事务,但是,PostgreSQL 需要跟踪FOR UPDATE锁是在保存点之后获取的事实,以便在后续 ROLLBACK TO SAVEPOINT 命令的情况下可以释放该锁。因此,会创建一个新的组合事务,并将其 ID 放置在行的xmax字段中。

总结

一个组合事务 ID 是一个内部标识符,用于支持多个事务的行锁定。

当事务使用 “SELECT … FOR UPDATE”(或者是锁定模式:SHARE、KEY SHARE、NO KEY UPDATE)锁定和更新元组时,将会创建组合事务 ID。

组合事务 ID 位于pg_multixact目录中。