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

Sam*_*ron 6 postgresql transactions isolation-level

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

update table set col = 1 where col = 2
Run Code Online (Sandbox Code Playgroud)

在默认READ COMMITTED隔离级别,来自多个并发会话,我保证:

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

Cra*_*ger 13

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

这个简单的案例

假设col1是唯一的,只有一个值"2",或者具有稳定的排序,所以每个UPDATE匹配相同的行以相同的顺序:

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

第一个tx将写入,提交并返回1的rowcount.提交将释放锁.

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

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

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

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

要了解更多信息,请阅读并发写入规则,PostgreSQL并发问题的第11页- 然后阅读该演示文稿的其余部分.

如果col1不是唯一的?

正如凯文在评论中指出的那样,如果col不是唯一的,那么你可能会匹配多行,那么不同的执行UPDATE可能会得到不同的排序.如果他们选择不同的计划(例如,一个是通过a PREPAREEXECUTE另一个是直接的,或者你正在搞乱enable_GUC)或者他们所有使用的计划都使用不稳定的相同值,那么就会发生这种情况.如果他们以不同的顺序获取行,那么tx1将锁定一个元组,tx2将锁定另一个元组,然后他们每个都会尝试锁定彼此已经锁定的元组.PostgreSQL将使用死锁异常中止其中一个.这是为什么所有数据库代码应始终准备重试事务的另一个好理由.

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

令人沮丧的是,PostgreSQL没有这么做,UPDATE ... ORDER BY因此确保您的更新始终以相同的顺序选择相同的行并不像您希望的那样简单.A SELECT ... FOR UPDATE ... ORDER BY后跟一个单独UPDATE的通常是最安全的.

更复杂的查询,排队系统

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

UPDATE test SET col = 1 WHERE col = (SELECT t.col FROM test t ORDER BY t.col LIMIT 1);
Run Code Online (Sandbox Code Playgroud)

或者其他建立一个简单的"队列"系统的努力*失败*以你的期望工作.有关更多信息,请参阅有关并发PostgreSQL文档此演示文稿.

如果您想要一个由数据库支持的工作队列,那么有经过充分测试的解决方案可以处理所有令人惊讶的复杂角落案例.最受欢迎的是PgQ.关于这个主题有一篇有用的PgCon论文,谷歌搜索'postgresql queue'充满了有用的结果.


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

  • @SamSaffron我刚刚测试过.除了其中一个tx中断了序列化失败,几乎是你所期望的.用`删除测试设置; insert into test(col)select generate_series(0,100); 开始; 锁表测试;`.建立你的竞争工作者:`开始隔离级别可串行; UPDATE测试SET col = 999999 WHERE col =(SELECT t.col FROM test t ORDER BY t.col LIMIT 1); COMMIT;`.然后通过在第一个tx中提交或回滚来解锁. (2认同)