在PostgreSQL中并发DELETE / INSERT锁定问题


35

这很简单,但是我对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锁定了实际的键值。

我对吗?可以使它起作用吗?

Answers:


39

Mat和Erwin都是正确的,我只是添加另一个答案,以用不适合发表评论的方式进一步扩展他们所说的内容。由于他们的回答似乎并不能使所有人满意,因此有人建议应咨询PostgreSQL开发人员,我是其中的一员,我将详细说明。

这里的重点是,在SQL标准下,在以事务READ COMMITTED隔离级别运行的事务中,限制是未提交的事务的工作必须不可见。已提交事务的工作可见时,将取决于实现。您所指出的是两种产品选择实施该方法的差异。两种实现都不会违反该标准的要求。

这是在PostgreSQL中发生的详细信息:

S1-1运行(删除1行)

旧行留在原处,因为S1可能仍会回滚,但是S1现在对该行持有锁,以便任何其他尝试修改该行的会话都将等待以查看S1是提交还是回滚。除非尝试使用或锁定表,否则对表的任何读取仍然可以看到旧行。SELECT FOR UPDATESELECT FOR SHARE

S2-1运行(但由于S1具有写锁定而被阻止)

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

S1-2运行(插入1行)

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

S1-3运行,释放写锁定

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

S2-1运行,现在它可以获得锁。但是报告删除了0行。嗯?

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

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

如果PostgreSQL从头开始使用新快照重新启动S2的整个DELETE语句,则其行为与SQL Server相同。由于性能原因,PostgreSQL社区未选择这样做。在这种简单的情况下,您永远不会注意到性能上的差异,但是如果DELETE在阻塞时您排成一千万行,那么您肯定会注意到。PostgreSQL在这里选择了性能,这是折衷方案,因为更快的版本仍然符合标准的要求。

S2-2运行,报告唯一的键约束冲突

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

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

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

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

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


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

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

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

21

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

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

你在插入该行S1的时候还不存在S2DELETE开始。因此,按上述S21)删除将不会看到它。而其中,S1删除是通过忽略S2DELETE根据(2)。

因此,在中S2,删除不起作用。但是,当插入出现时,确实会看到S1的插入:

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

因此,尝试插入的操作因S2约束违反而失败。

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

但是,这将允许您重试事务。


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

11

我完全同意@Mat的出色回答。我只写另一个答案,因为它不适合评论。

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

   S1删除成功  
S2 DELETE(通过代理成功-从S1进行DELETE)  
   S1 实际上同时重新插入删除的值  
S2 INSERT因唯一键约束冲突而失败

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



-2

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.