Postgres事务中的锁定

11
我不太理解锁定如何与Postgres事务互动。
当我运行这个(冗长的)查询时,我惊讶于发生的大量锁定:
BEGIN;
TRUNCATE foo;
\COPY foo FROM 'backup.txt';
COMMIT;

文档中关于\COPY命令并未提及所需的锁级别,但此帖子表明它只获取RowExclusiveLock。但是,当我在执行\COPY命令时运行此查询:

SELECT mode, granted FROM pg_locks
WHERE relation='foo'::regclass::oid;

我得到了这个:

mode    granted
RowExclusiveLock    true
ShareLock   true
AccessExclusiveLock true

这个AccessExclusiveLock到底是从哪里来的呢?我猜它是来自TRUNCATE,需要一个AccessExclusiveLock。但是TRUNCATE很快就完成了,所以我希望锁也能很快释放。这让我有几个问题。
当一个命令在事务中获取锁时,该锁是否会在命令结束时(在事务结束之前)被释放?如果是这样,为什么我观察到上述行为?如果不是,为什么不是?实际上,由于事务在COMMIT之前不会触及表格, 那么为什么在事务中的TRUNCATE需要完全阻塞表格呢?
我在PG的交易文档没有看到任何关于这方面的讨论。
1个回答

26

这里有几个误解需要澄清。

首先,在提交之前,事务确实会涉及到表。你引用的评论说的是ROLLBACK(以及COMMIT)不会涉及到表,这是不同的事情。它们会在提交日志(在pg_clog中)中记录事务状态,而COMMIT会将事务日志刷新到磁盘上(其中一个显著的例外是TRUNCATE,这与您的问题相关:旧表会一直保留到事务结束,并在COMMIT期间删除)。

如果所有更改都被推迟到COMMIT时再进行,并且不会采取任何锁定,那么COMMIT将非常昂贵,并且通常会因为并发修改而失败。事务必须记住数据库在之前的状态,并检查更改是否仍然适用。这种处理并发的方式称为乐观并发控制,虽然它是一个适用于应用程序的不错的策略,但对于关系型数据库来说效果不佳,因为COMMIT应该高效且不应失败(除非基础设施存在重大问题)。
因此,关系型数据库使用悲观并发控制锁定,即在访问数据库对象之前锁定它们,以防止并发活动妨碍它们。
其次,关系型数据库使用两段锁定。其中的锁定(至少是用户可见的重量级锁)会一直保留到事务结束。这是保持事务逻辑顺序(可串行化)和一致性所必需的(但不充分)。如果你释放了一个锁,并且其他人通过外键约束删除了你插入但未提交的行所指向的行呢?
所有这些的结果是,您的表将保留来自TRUNCATEACCESS EXCLUSIVE锁,直到事务结束。难道不明显为什么这是必要的吗?如果允许其他事务在(尚未提交的)TRUNCATE之后甚至读取表,则会发现表为空,因为TRUNCATE确实清空了表并且不遵循MVCC语义。这种(未提交可能仍被回滚的数据的)脏读取是不允许的。
如果您在重新填充期间确实需要读取表中的数据,可以使用DELETE而不是TRUNCATE。缺点是这是一种更昂贵的操作,会使表格留下许多“死元组”,必须由自动清理来删除,导致大量空白空间(表膨胀)。但是,如果您愿意使用一个表和索引,它们膨胀到至少需要两倍的时间才能进行表和索引扫描,则可以选择此选项。

1
谢谢,@Laurenz!我仍在努力理解事务中的TRUNCATE如何工作。如果"TRUNCATE真的清空了表",而"ROLLBACK不会影响表",那么TRUNCATE如何被回滚? - Simon Lepkin
我也不太明白你所说的“脏读”是什么意思(我在文档中看到了相关内容),但这可能需要另一个问题来解决。 - Simon Lepkin
1
我解释了“脏读”,并添加了一个链接。你说得对,当涉及到TRUNCATE时,我的解释有点不一致。如代码所述,旧表不会立即被丢弃,而是在提交时才会被删除,因此在这种情况下,COMMIT并没有真正触及表格,而是将其移除。我会尝试在我的解释中更清楚地表达这一点。 - Laurenz Albe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接