为什么PostgreSQL的 TRUNCATE TABLE在 MVCC下不安全?
在调试我们系统中的一个微妙问题时,我意识到 TRUNCATE TABLE 在并发事务看到空表方面引发了问题。
重新查看文档,我发现文档明确说明它并非MVCC安全。
TRUNCATE不是MVCC安全的。截断后,对使用截断发生前所创建的快照的并发事务来说,该表将显示为空。更多细节请参阅第13.5节。
为什么会这样?
解决方案
如果你不需要一次清除多张表,并且你确实需要MVCC安全,请使用一个简单的未限定的 delete:db<>fiddle的演示
delete from yourtable;
truncate 的多目标特性必须通过如下方式,设置一系列单独的 delete 来模拟。如果你需要把它们全部作为一个查询来执行,请将它们包装在CTE中。
Delete 会遍历每个保存表数据的文件,并留下一个带有你的事务标识符的标记,表示所有比你新的事务应当发现该表已被清空。所有仍在进行中的事务的并发会话可以忽略该标记,并继续使用它们之前的快照,直到它们 commit/rollback。
Truncate 相反地只是创建一个空文件,并将表重新链接到它,从而分离并让旧文件进入垃圾回收——在这里,每个人的MVCC “书签” 规定谁看到什么。你可以在 src/backend/commands/tablecmds.c:2230 中看到它。
c /*(...) * Create a new empty storage file for the relation, and assign it * as the relfilenumber value. The old storage file is scheduled * for deletion at commit. */ RelationSetNewRelfilenumber(rel, rel->rd_rel->relpersistence);
它不是MVCC安全的,但它是事务安全的,因此尽管表的“看起来像被清空”是即时的,磁盘空间的释放实际上是在围绕 truncate 的事务提交时才会安排发生,且只有在该事务提交时才发生,而不是在 truncate 命令完成后立即发生。
c /* * Schedule unlinking of the old storage at transaction commit, except * when performing a binary upgrade, when we must do it immediately. */ if (IsBinaryUpgrade) {/*(...)*/ } else { /* Not a binary upgrade, so just schedule it to happen later. */ RelationDropStorage(relation); }
作为不切换到普通的 delete 的替代方案,你可以确保使用你计划对其进行 truncate 的表的其他会话请求并对其锁定一些锁,直到它们完成。一个 truncate 将尝试获取一个 access exclusive 锁,因此它只会等待在它之前发起的会话完成对目标对象的操作,并阻塞新来的会话。
TRUNCATE在它操作的每张表上获取一个ACCESS EXCLUSIVE锁,这会阻塞该表的所有其他并发操作。当指定RESTART IDENTITY时,任何需要重启的序列也将被同样的独占锁锁定。如果需要对同一表进行并发访问,则应改用DELETE命令。
ACCESS EXCLUSIVE (AccessExclusiveLock)与所有模式的锁冲突(
ACCESS SHARE、ROW SHARE、ROW EXCLUSIVE、SHAREUPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE、和ACCESS EXCLUSIVE)。此模式保证持有者在任何方面都是唯一访问该表的事务。 通过DROP TABLE、TRUNCATE、REINDEX、CLUSTER、VACUUM FULL和REFRESH MATERIALIZED VIEW(不带CONCURRENTLY)命令获得。许多形式的ALTER INDEX和ALTER TABLE也在此级别获取锁。这也是对未明确指定模式的LOCK TABLE语句的默认锁模式。提示
只有ACCESS EXCLUSIVE锁会阻止一个SELECT(不带FOR UPDATE/SHARE)语句。