六月 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 指向事务772和773。 | |
| COMMIT; | 事务 773现在已结束,但该行的xmax值仍为14。由于组合事务是不可变的,因此组合事务14仍然会指向现已失效的事务773,以及正在进行中的事务772。 | |
| COMMIT; | 两个事务都结束后,在该行上不再有处于活动状态的锁。它的 xmax值仍然是14,并且会一直保持为14,直到另一个进程锁定该行,或者该表被进行 VACUUM。 | 
值得重申的是,组合事务是不可变的。如果两个事务104和108,分别作为组合事务19的一部分,都锁定了 R 行,并且事务117也要锁定该行,则事务117不能简单地联接组合事务19。相反,要创建一个 ID 为20的新的组合事务,其中包含了事务104、108和117。
这意味着,每当一个额外的事务想要锁定一行时,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目录中。