多个线程可以在约束集上导致重复更新吗?

时间:2012-08-11 12:42:11

标签: postgresql transactions isolation-level

在postgres中,如果我运行以下语句

update table set col = 1 where col = 2

在默认READ COMMITTED isolation level中,来自多个并发会话,我保证:

  1. 在单个匹配的情况下,只有1个线程将获得ROWCOUNT为1(意味着只有一个线程写入)
  2. 在多重匹配的情况下,只有一个主题将获得ROWCOUNT> 0(意味着只有一个线程写入批处理)

1 个答案:

答案 0 :(得分:13)

您声明的保证适用于这种简单的情况,但不一定适用于稍微复杂的查询。有关示例,请参阅答案的结尾。

简单案例

假设col1是唯一的,只有一个值“2”,或者具有稳定的排序,因此每个UPDATE按相同的顺序匹配相同的行:

这个查询会发生什么,线程会找到col = 2的行,并且所有人都试图在该元组上获取写锁。其中只有一个会成功。其他人将阻止等待第一个线程的事务提交。

第一个tx将写入,提交并返回1的行数。提交将释放锁。

其他tx将再次尝试抢锁。他们一个接一个地成功。每笔交易将依次经过以下过程:

  • 获取有争议的元组的写锁定。
  • 获取锁定后重新检查WHERE col=2条件。
  • 重新检查会显示条件不再匹配,因此UPDATE会跳过该行。
  • UPDATE没有其他行,因此会报告更新的零行。
  • 提交,释放下一个tx的锁,试图抓住它。

在这种简单的情况下,行级锁定和条件重新检查有效地序列化了更新。在更复杂的情况下,不是那么多。

您可以轻松证明这一点。打开说四个psql会话。在第一个中,使用BEGIN; LOCK TABLE test; * 锁定表格。在其余的会话中运行相同的UPDATE - 它们将阻止表级锁定。现在通过COMMIT第一个会话释放锁定。看他们比赛。只有一个会报告行数为1,其他人将报告为0.这很容易自动化并编写脚本以便重复和扩展到更多连接/线程。

要了解详情,请阅读rules for concurrent writing第11页的PostgreSQL concurrency issues - 然后阅读该演示文稿的其余部分。

如果col1是非唯一的?

正如Kevin在评论中指出的那样,如果col不是唯一的,那么您可能会匹配多行,那么UPDATE的不同执行可能会得到不同的排序。如果他们选择不同的计划(例如,一个是通过PREPAREEXECUTE而另一个是直接的,或者您正在弄乱enable_ GUC)或计划他们所有使用都使用不稳定的相等值。如果他们以不同的顺序获取行,那么tx1将锁定一个元组,tx2将锁定另一个元组,然后他们每个都会尝试锁定彼此已经锁定的元组。 PostgreSQL将使用死锁异常中止其中一个。这是所有数据库代码总是准备重试事务的另一个好理由。

如果你小心确保并发UPDATE总是以相同的顺序获得相同的行,你仍然可以依赖于答案第一部分中描述的行为。

令人沮丧的是,PostgreSQL不提供UPDATE ... ORDER BY,因此确保您的更新始终以相同的顺序选择相同的行并不像您希望的那样简单。 <{1}}后跟单独的SELECT ... FOR UPDATE ... ORDER BY通常是最安全的。

更复杂的查询,排队系统

如果您正在使用多个阶段进行查询,涉及多个元组或除了相等的条件之外的条件,则可能会得到与串行执行结果不同的令人惊讶的结果。特别是,并发运行如下:

UPDATE

或构建简单“队列”系统的其他工作 *失败*以达到预期效果。有关详细信息,请参阅PostgreSQL docs on concurrencythis presentation

如果您想要一个由数据库支持的工作队列,那么有经过充分测试的解决方案可以处理所有令人惊讶的复杂角落案例。其中最受欢迎的是PgQ。关于该主题有一个有用的PgCon papera Google search for 'postgresql queue'充满了有用的结果。


* BTW,而不是UPDATE test SET col = 1 WHERE col = (SELECT t.col FROM test t ORDER BY t.col LIMIT 1); ,您可以使用LOCK TABLE来获取对元组上的写锁定。这将阻止对它的更新,但不阻止写入其他元组或阻止任何读取。这允许您模拟不同类型的并发问题。