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

Dav*_*Bob 38 postgresql concurrency locking serialization

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

CREATE TABLE test (id INT PRIMARY KEY);
Run Code Online (Sandbox Code Playgroud)

和几行:

INSERT INTO TEST VALUES (1);
INSERT INTO TEST VALUES (2);
Run Code Online (Sandbox Code Playgroud)

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

每个相同的代码位:

1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;
Run Code Online (Sandbox Code Playgroud)

现在,以慢动作运行它,在窗口中一次执行一个。

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
Run Code Online (Sandbox Code Playgroud)

现在,这在 SQLServer 中运行良好。当 S2 进行删除时,它报告删除了 1 行。然后 S2 的插入工作正常。

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

我对吗?这可以工作吗?

kgr*_*ttn 45

Mat 和 Erwin 都是对的,我只是添加了另一个答案,以一种不适合评论的方式进一步扩展他们所说的内容。由于他们的回答似乎并没有让大家满意,并且有建议咨询PostgreSQL开发人员,我也是其中之一,我会详细说明。

这里的重点是,在 SQL 标准下,在READ COMMITTED事务隔离级别运行的事务中,限制是未提交事务的工作必须不可见。提交事务的工作何时变得可见取决于实现。您所指出的是两种产品选择实现这一点的方式不同。两种实现都没有违反标准的要求。

以下是 PostgreSQL 中发生的事情,详细说明:

S1-1 运行(删除了 1 行)

旧行留在原地,因为 S1 可能仍会回滚,但 S1 现在持有该行的锁,以便任何其他尝试修改该行的会话将等待查看 S1 是提交还是回滚。对表的任何读取仍然可以看到旧行,除非他们尝试使用SELECT FOR UPDATE或锁定它SELECT 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在被阻塞时进入了 1000 万行,您肯定会注意到。在 PostgreSQL 选择性能的情况下,这里有一个权衡,因为更快的版本仍然符合标准的要求。

S2-2 运行,报告唯一键约束违规

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

虽然这里有一些令人惊讶的行为,但一切都符合 SQL 标准,并且在标准的“特定于实现”的范围内。如果您假设某些其他实现的行为将出现在所有实现中,那当然会令人惊讶,但 PostgreSQL 非常努力地避免READ COMMITTED隔离级别中的序列化失败,并允许一些与其他产品不同的行为以实现这一目标。

现在,我个人不是任何产品实现中READ COMMITTED事务隔离级别的忠实拥护者。从事务的角度来看,它们都允许竞争条件产生令人惊讶的行为。一旦有人习惯了一种产品所允许的怪异行为,他们往往会认为这是“正常的”,而另一种产品选择的权衡是奇怪的。但是每个产品都必须对任何实际上没有实现为. PostgreSQL 开发人员选择划清界限的地方是最小化阻塞(读不阻塞写,写不阻塞读)和最小化序列化失败的机会。SERIALIZABLEREAD COMMITTED

该标准要求SERIALIZABLE事务是默认值,但大多数产品不这样做,因为它会导致性能受到更宽松的事务隔离级别的影响。有些产品在SERIALIZABLE被选中时甚至不提供真正可序列化的事务——最显着的是 Oracle 和 9.1 之前的 PostgreSQL 版本。但是使用真正的SERIALIZABLE事务是避免竞争条件产生意外影响的唯一方法,并且SERIALIZABLE事务总是必须要么阻塞以避免竞争条件,要么回滚一些事务以避免发展中的竞争条件。SERIALIZABLE事务的最常见实现是严格两阶段锁定 (S2PL),它同时具有阻塞和序列化失败(以死锁的形式)。

全面披露:我与麻省理工学院的 Dan Ports 合作,使用称为 Serializable Snapshot Isolation 的新技术将真正可序列化的事务添加到 PostgreSQL 9.1 版。


Mat*_*Mat 21

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

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

你在插入该行S1的时候还不存在S2DELETE开始。所以它不会被上面的S2( 1 ) 中的删除看到。根据(2),S1被删除的被S2's忽略。DELETE

所以在 中S2,删除什么都不做。当插入出现时,那个人确实看到了S1插入:

因为 Read Committed 模式以一个新的快照启动每个命令,其中包括到该时刻提交的所有事务,所以同一事务中的后续命令在任何情况下都将看到已提交并发事务的影响。上面的问题在于单个命令是否看到了绝对一致的数据库视图。

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

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

这将允许您重试事务。


Erw*_*ter 11

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

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

   S1 删除成功  
S2 DELETE(通过代理成功 - 从 S1 删除)  在此期间, 
   S1几乎重新插入已删除的值  
S2 INSERT 因违反唯一键约束而失败

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


Fra*_*ens 0

使用DEFERRABLE主键并重试。