六月 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
目录中。