这很简单,但是我对PG(v9.0)的功能感到困惑。
我们从一个简单的表开始:

CREATE TABLE test (id INT PRIMARY KEY);


和一个几行:

INSERT INTO TEST VALUES (1);
INSERT INTO TEST VALUES (2);


使用我最喜欢的JDBC查询工具(ExecuteQuery),我将两个会话窗口连接到该表所在的数据库。它们都是事务性的(即auto-commit = false)。我们分别称它们为S1和S2。

每个代码都具有相同的代码:

1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;


现在,以慢速运行此代码,执行一个一次在Windows中运行。

S1-1 runs (1 row deleted)
S2-1 runs (but is blocked since S1 has a write lock)
S1-2 runs (1 row inserted)
S1-3 runs, releasing the write lock
S2-1 runs, now that it can get the lock. But reports 0 rows deleted. HUH???
S2-2 runs, reports a unique key constraint violation


现在,这在SQLServer中可以正常工作。当S2进行删除时,它报告1行已删除。然后S2的插入工作正常。

我怀疑PostgreSQL正在锁定该行所在的表中的索引,而SQLServer则锁定了实际的键值。

我正确吗? ?可以使它起作用吗?

#1 楼

Mat和Erwin都是对的,我只是添加另一个答案,以用不适合发表评论的方式进一步扩展他们所说的内容。由于他们的回答似乎并不能使所有人满意,因此有人建议应咨询PostgreSQL开发人员,我是其中的一个,我将详细说明。在以事务隔离级别运行的事务中,限制是未提交事务的工作必须不可见。当已提交事务的工作可见时,将取决于实现。您所指出的是两种产品选择实施该方法的差异。两种实现都没有违反该标准的要求。

这是PostgreSQL内部的详细信息:位置,因为S1可能仍会回滚,但是S1现在在该行上拥有一个锁,以便任何其他尝试修改该行的会话都将等待以查看S1是提交还是回滚。除非尝试使用READ COMMITTEDSELECT FOR UPDATE锁定表,否则对表的任何读取仍然可以看到旧行。

S1-1 runs (1 row deleted)


S2现在必须等待以查看结果S1。如果S1将回滚而不是提交,则S2将删除该行。请注意,如果S1在回滚之前插入了新版本,那么从任何其他事务的角度来看,新版本都不会存在,从任何其他事务的角度来看,旧版本也不会被删除。

S2-1 runs (but is blocked since S1 has a write lock)


该行独立于旧行。如果存在id = 1的行的更新,则旧版本和新版本将相关,并且S2可以在该行的未阻止状态删除该行的更新版本。新行恰好具有与过去存在的某行相同的值,这使其与该行的更新版本不同。

S1-2 runs (1 row inserted)


因此S1的更改得以保留。一行不见了。已添加一行。

S1-3 runs, releasing the write lock


内部发生的事情是,存在从一个行的一个版本指向同一行的下一个版本的指针。更新。如果该行被删除,则没有下一个版本。当SELECT FOR SHARE事务在发生写冲突时从一个块中唤醒时,它将跟随该更新链到达末尾。如果尚未删除该行,并且该行仍符合查询的选择条件,则将对其进行处理。该行已被删除,因此S2的查询继续进行。

S2在扫描表期间可能会或可能不会到达新行。如果是这样,它将看到新行是在S2的READ COMMITTED语句启动之后创建的,因此它也不是它可见的行集合的一部分。

如果PostgreSQL重新启动S2的整个DELETE语句从新快照开始,它的行为与SQL Server相同。由于性能原因,PostgreSQL社区未选择这样做。在这种简单的情况下,您永远不会注意到性能的差异,但是如果在阻塞时将一千万行插入到DELETE中,您肯定会注意到。 PostgreSQL选择性能时需要权衡,因为更快的版本仍然符合标准的要求。

S2-1 runs, now that it can get the lock. But reports 0 rows deleted. HUH???


当然,该行已经存在。这是图片中最令人惊讶的部分。

虽然这里有一些令人惊讶的行为,但是所有内容都符合SQL标准,并且在该标准的“特定于实现”的范围内。如果您假设所有实现中都会存在某些其他实现的行为,那肯定会令人惊讶,但是PostgreSQL尽力避免在DELETE隔离级别上发生序列化失败,并允许某些行为不同于其他产品以实现该目的。 。

现在,就我个人而言,我不喜欢任何产品实现中的READ COMMITTED事务隔离级别。从交易的角度来看,它们都允许种族条件产生令人惊讶的行为。一旦某人习惯了一种产品所允许的怪异行为,他们就会倾向于认为“正常”,而另一种产品所选择的取舍则很奇怪。但是,每种产品都必须对实际上未实现为READ COMMITTED的任何模式进行某种折衷。 PostgreSQL开发人员选择在SERIALIZABLE中划清界限的地方是最大程度地减少阻塞(读取不阻塞写入,写入不阻塞读取)并最小化序列化失败的机会。

标准要求READ COMMITTED事务是默认事务,但是大多数产品不这样做,因为它会导致事务隔离级别更高的性能下降。选择SERIALIZABLE时,某些产品甚至不提供真正可序列化的事务-最著名的是Oracle和9.1之前的PostgreSQL版本。但是,使用真正的SERIALIZABLE事务是避免竞争条件带来的意外影响的唯一方法,并且SERIALIZABLE事务始终必须要么阻塞以避免竞争条件,要么回滚某些事务以避免发展的竞争条件。 SERIALIZABLE事务的最常见实现是严格的两阶段锁定(S2PL),它同时具有阻塞和序列化失败(以死锁的形式)。

全面披露:我与MIT的Dan Ports合作使用一种称为“可序列化快照隔离”的新技术,将真正可序列化的事务添加到PostgreSQL 9.1版。

评论


我想知道进行这项工作的真正便宜(便宜吗?)方法是发出两个DELETES,然后插入INSERT。在我有限的(2个线程)测试中,它可以正常工作,但需要进行更多测试,以了解这是否适用于许多线程。

–戴维鲍勃
2012年10月29日11:56



只要您使用的是READ COMMITTED事务,就处于竞争状态:如果在第一个DELETE开始之后和第二个DELETE开始之前另一个事务插入了新行,将会发生什么情况?对于不严格于SERIALIZABLE的事务,关闭竞争条件的两种主要方法是通过促进冲突(但这在删除行时无济于事)和实现冲突。您可以通过删除每行更新的“ id”表或显式锁定表来实现冲突。或对错误使用重试。

– kgrittn
2012年10月29日12:34

重试它。非常感谢您的宝贵见解!

–戴维鲍勃
2012年10月29日17:22

#2 楼

我相信这是设计使然,根据对PostgreSQL 9.2的已提交读隔离级别的描述:


UPDATE,DELETE,SELECT FOR UPDATE和SELECT FOR SHARE命令的行为相同在搜索目标行方面与SELECT相同:它们只会找到从命令开始时间1开始提交的目标行。但是,这样的目标行在被发现时可能已经被另一个并发事务更新(或删除或锁定)。在这种情况下,可能的更新程序将等待第一个更新事务提交或回滚(如果仍在进行中)。如果第一个更新程序回滚,则其效果将被否定,第二个更新程序可以继续更新最初找到的行。如果第一个更新程序提交,则第二个更新程序将忽略该行(如果第一个更新程序删除了该行),否则它将尝试将其操作应用于该行的更新版本。


S1S2启动时,您在DELETE中插入的行尚不存在。因此,按上述(1)进行删除在S2中将看不到它。 S1S2根据(2)会忽略DELETE删除的那个。

因此在S2中,删除不执行任何操作。但是,当插入操作出现时,确实会看到S1的插入操作:


因为“读取已提交”模式以一个新快照开始了每个命令,该快照包含了该时刻之前提交的所有事务,因此无论如何,同一事务中的命令将看到已提交的并发事务的效果。上面的问题是单个命令是否看到数据库的绝对一致视图。


因此S2尝试插入会失败,并违反约束。

继续读取文档,使用可重复读取甚至可序列化都无法完全解决您的问题-第二个会话将失败,并在delete上出现序列化错误。

这将使您可以重试事务。

评论


谢谢Mat。尽管这似乎确实正在发生,但这种逻辑似乎存在缺陷。在我看来,在READ_COMMITTED iso级别上,这两个语句必须在tx内成功执行:DELETE FROM test WHERE ID = 1 INSERT INTO test VALUES(1)我的意思是,如果我删除该行然后插入该行,那么插入应该成功。 SQLServer获得此权利。实际上,在必须同时使用两个数据库的产品中,我很难处理这种情况。

–戴维鲍勃
2012年10月26日18:50

#3 楼

我完全同意@Mat的出色回答。我只写另一个答案,因为它不能放在注释中。

回复您的评论:S2中的DELETE已经挂接到特定的行版本上。由于这同时被S1杀死,因此S2认为自己成功。尽管乍看之下并不明显,但一系列事件实际上是这样的:

   S1 DELETE successful  
S2 DELETE (successful by proxy - DELETE from S1)  
   S1 re-INSERTs deleted value virtually in the meantime  
S2 INSERT fails with unique key constraint violation


这都是设计使然。您确实需要根据需求使用SERIALIZABLE事务,并确保重试序列化失败。

#4 楼

使用DEFERRABLE主键,然后重试。

评论


感谢您的提示,但使用DEFERRABLE完全没有区别。该文档应具有应有的读法,但没有。

–戴维鲍勃
2012年10月26日18:32

#5 楼

我们也面临这个问题。我们的解决方案是在select ... for update之前添加delete from ... where。隔离级别必须为“已读”。